Skip to main content

dune_core/
pss.rs

1//! `.pss` — periodic steady state of a driven circuit by shooting
2//! (issue #13, D-0028 track 4). No 3f5 contract: the analysis is new.
3//! ngspice-47 has a `.pss` card (Perticaroli/Lannutti,
4//! `analysis/dcpss.c`), but its algorithm is a free-running
5//! oscillator search — it re-estimates the frequency from the
6//! transient on every cycle and never solves `x(T) = x(0)`, so on a
7//! driven circuit it drifts off the drive frequency (a 1 kHz RC
8//! comes out at 750 Hz). Dune keeps ngspice's card and plot names
9//! and replaces the method (docs/dune-core.md, "Regime permanente
10//! periódico").
11//!
12//! **Method.** Shooting–Newton on the period map. With `Φ(x₀)` the
13//! state a transient started from `x₀` at `t₀` reaches at `t₀ + T`,
14//! the periodic steady state solves `F(x₀) = Φ(x₀) − x₀ = 0`;
15//! Newton's step solves `(I − Φ′) δ = Φ(x₀) − x₀` with the monodromy
16//! matrix `Φ′`. `Φ′` is never formed: GMRES needs only the products
17//! `Φ′ v`, each one directional finite difference of two transients
18//! retraced on the frozen grid of the current iterate (matrix-free
19//! shooting, Telichevesky–Kundert–White 1995). A circuit with `k`
20//! slow modes needs about `k + 1` products per Newton step, against
21//! `n` for a dense finite-difference Jacobian; a linear circuit
22//! converges in one Newton step.
23//!
24//! **Transient map.** Every shooting run is a fresh engine running
25//! the full `DCtran` loop (LTE control, breakpoints, trap/Gear/
26//! TR-BDF2, the step budget of D-0022) from the state `x₀` instead
27//! of an operating point: the device states are seeded from `x₀`
28//! ([`Engine::tran_period`]), so charges and junction voltages are
29//! functions of `x₀` alone — the full MNA vector is the shooting
30//! state, and its algebraic components fall out of the Jacobian on
31//! their own (their columns of `Φ′` vanish).
32//!
33//! **Autonomous oscillators** (no periodic source, issue #20): the
34//! period `T` is a Newton unknown, the phase pinned by holding
35//! OSCNODE at its value on a rising mid-range crossing — see
36//! `Circuit::pss_autonomous` and docs/dune-core.md.
37//!
38//! **Transmission lines** (issue #20): a line's state is its history,
39//! and in the periodic regime the history is the orbit itself,
40//! `x(t − τ) = x(t − τ + kT)`. The lines read the previous orbit as
41//! their past (`matrix::PeriodicPast`), a fixed point iterated (Picard) on a
42//! frozen step grid while Newton solves `x₀` — see
43//! `Shooter::lines_excited`. Lossy LTRA lines need `.options
44//! ltrafit` (their infinite periodic past is summed in closed form by
45//! the recursive model) and have their DC drop pinned (`ltra.rs`).
46
47use 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
58/// Plot name of the period in the time domain (ngspice-47's).
59pub const PSS_TIME_PLOT: &str = "Time Domain Periodic Steady State Analysis";
60/// Plot name of the harmonics (ngspice-47's).
61pub const PSS_FREQ_PLOT: &str = "Frequency Domain Periodic Steady State Analysis";
62
63/// The `.pss` request (see [`Circuit::pss`]).
64#[derive(Clone, Debug, PartialEq)]
65pub struct PssParams {
66    /// Fundamental frequency in Hz; `None` takes it from the
67    /// periodic sources (the greatest common divisor of their
68    /// frequencies). Without periodic sources (an autonomous
69    /// oscillator) it is a guess of the period's inverse, and `None`
70    /// estimates it from the stabilization transient.
71    pub freq: Option<f64>,
72    /// Stabilization time: a plain transient runs from 0 to the first
73    /// period boundary at or after `tstab` (at least one period)
74    /// before the shooting starts there.
75    pub tstab: f64,
76    /// ngspice's oscillation node. A driven circuit only checks and
77    /// reports it; an autonomous one (no periodic source) needs it:
78    /// its phase condition holds this node at its value on a rising
79    /// mid-range crossing, and its crossings estimate the period.
80    pub osc_node: Option<String>,
81    /// Samples per period in the time-domain plot (the plot has
82    /// `points + 1` rows, both ends included, like ngspice's).
83    pub points: usize,
84    /// Rows of the frequency-domain plot: DC plus `harmonics − 1`
85    /// harmonics (ngspice's count).
86    pub harmonics: usize,
87    /// Most shooting iterations (adaptive period transients).
88    pub max_iter: usize,
89    /// Scale of the convergence tolerance: converged when every
90    /// unknown has `|x(t₀+T) − x(t₀)| ≤ steady_coeff · (RELTOL ·
91    /// peak + VNTOL|ABSTOL)`, `peak` its largest magnitude over the
92    /// period.
93    pub steady_coeff: f64,
94    /// Run the stabilization transient with `UIC`.
95    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/// Result of a periodic steady-state analysis.
114#[derive(Clone, Debug)]
115pub struct Pss {
116    /// One period sampled at `points + 1` equispaced times from `t₀`
117    /// to `t₀ + T` ([`PSS_TIME_PLOT`], scale `time`; node voltages and
118    /// branch currents as in `.tran`).
119    pub time: Solution,
120    /// The complex harmonics `X_k`, `k = 0 … harmonics − 1`, at
121    /// `k·f` ([`PSS_FREQ_PLOT`], scale `frequency`): `X_0` the mean,
122    /// and `x(t) = X_0 + Σ Re(X_k e^{jkωt})` — `.ac`'s phasor
123    /// convention, phases referred to `t = 0`. ngspice writes only
124    /// the magnitudes; `mag()` of these columns is that plot.
125    pub spectrum: Solution,
126    /// The period `T` (s): the drive's for a driven circuit, the
127    /// shooting's result for an autonomous oscillator.
128    pub period: f64,
129    /// Start of the reported period (a multiple of `T`).
130    pub t0: f64,
131    /// The residual `max_i |x_i(t₀+T) − x_i(t₀)| / tol_i` of each
132    /// shooting iteration (converged when ≤ 1).
133    pub residuals: Vec<f64>,
134    /// The period of each shooting iteration, aligned with
135    /// `residuals` (constant for a driven circuit; the Newton iterate
136    /// `T` of an autonomous one).
137    pub periods: Vec<f64>,
138    pub notes: Vec<String>,
139}
140
141/// The perturbation of the directional differences, in units of the
142/// convergence tolerance (the scaled unknowns are `x_i / tol_i`).
143const FD_EPS: f64 = 1e-2;
144/// The replays of the Jacobian products tighten the Newton
145/// tolerances of every step by this factor: a frozen-grid replay is
146/// smooth in `x₀` only up to its Newton error, which the difference
147/// would amplify by `1/ε`.
148const REPLAY_TIGHTEN: f64 = 1e-3;
149/// Most halvings of a Newton step whose residual did not decrease.
150const MAX_BACKTRACK: usize = 4;
151
152impl Circuit {
153    /// Run `.pss`: the periodic steady state of the circuit driven at
154    /// the fundamental `p.freq` (or the one its sources share), by
155    /// shooting–Newton with matrix-free GMRES on the period map (see
156    /// the module docs). Fails with [`Error::PssConvergence`] when
157    /// `p.max_iter` shooting iterations do not bring the period
158    /// mismatch within tolerance.
159    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            // Issue #22: the same reason as the TRNOISE/TRRANDOM
178            // sources below. The option used to be ignored here
179            // (only `.tran` installs the generators), so the result
180            // looked as if it had taken the noise into account.
181            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                // Issue #13, part 2: a random stimulus has no periodic
195                // steady state; shooting would chase a moving target.
196                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                // Issue #20: the periodic past of a lossy line is
207                // infinite; only the recursive model sums it.
208                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            // No periodic stimulus: a free-running oscillator, its
229            // period an unknown of the shooting (see `pss_autonomous`).
230            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        // Stabilization: a plain transient to t0.
267        let built = self.build(opts)?;
268        notes.extend(built.notes.iter().cloned());
269        let mut engine = Engine::new(built);
270        // Work guards (hostile cards must cost nothing, CLAUDE.md
271        // princípio 4): a period transient keeps about `points` rows,
272        // the stabilization takes `n_stab · points` steps, the DFT
273        // `harmonics · points` products per unknown.
274        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        // With lines, the last stabilization period is the first guess
294        // of the periodic past (only the rows kept change).
295        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            // The last Newton step taken: (base point, step, base residual, fraction).
336            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                // A transient that fails from a Newton iterate (an
340                // overshoot into a region the step control cannot cross)
341                // counts as an infinite residual and is backtracked.
342                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                // Backtrack a Newton step that did not reduce the residual.
366                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    /// The port unknowns of the transmission lines (issue #20) — the
415    /// four terminals and the two port-current equations of every
416    /// T/O line, ground left out —, whose orbit must reproduce the
417    /// periodic past the lines read.
418    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    /// The frequencies the independent sources repeat at: SIN and
445    /// SFFM carriers/modulations, PULSE `1/PER`. Waveforms that settle
446    /// to a constant (EXP, PWL, a PULSE without PER) set none. A SIN
447    /// or SFFM without an explicit frequency (its default is
448    /// `1/TSTOP`) or a damped SIN has no period and is an error.
449    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                // Refused by `Circuit::pss` before the harvest.
503                Wave::TrNoise(_) | Wave::TrRandom(_) => {}
504            }
505        }
506        Ok(out)
507    }
508}
509
510impl Circuit {
511    /// `.pss` of an autonomous oscillator (no periodic source): the
512    /// period is an unknown. Shooting–Newton on the augmented system
513    /// `[Φ_T(x₀) − x₀ = 0; x₀[osc] = v_ref]`, the phase condition
514    /// fixing `OSCNODE` at its value on a rising crossing of its
515    /// mid-range (see [`osc_start`]); the bordered Jacobian
516    /// `[I − Φ′, −∂Φ/∂T]` (osc column dropped) is solved by the same
517    /// matrix-free GMRES, `∂Φ/∂T` by a replay on the frozen grid
518    /// stretched to `T + δT`.
519    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        // Stabilization grid: with a guess, the driven one; without,
551        // TMAX = TSTAB/STAB_POINTS (the LTE control refines it).
552        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        // Lines: a continuation from the stabilization (the refine
579        // run, and the shooting) reads the last period before it as
580        // its periodic past.
581        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            // The stabilization grid knew no period: refine the
601            // estimate (and the start) on a short continuation at
602            // the shooting's own grid, T/POINTS, from where it ended.
603            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                // At least one Newton step: an estimate already within
700                // the tolerance still gets its T corrected (quadratically)
701                // — the mismatch criterion alone admits a period error of
702                // about tol/|dx/dt| — and the step's δT is reported.
703                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                // A Newton step never moves T by more than MAX_DT of itself
728                // (far from the orbit the linearized phase drift is not
729                // to be trusted); the whole step is scaled.
730                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        // The trivial periodic solution — the equilibrium — is
753        // periodic for every T; a converged "orbit" with no swing at
754        // OSCNODE is that, not an oscillation.
755        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
797/// Default stabilization grid of an autonomous `.pss` without a
798/// frequency guess: TMAX = TSTAB / this.
799const STAB_POINTS: f64 = 1000.0;
800/// The fraction of TSTAB (its end) the stabilization keeps and
801/// measures the oscillation on.
802const STAB_WINDOW: f64 = 0.5;
803/// Rising crossings the period estimate averages over (at most).
804const EST_PERIODS: usize = 4;
805/// Length, in estimated periods, of the continuation that refines a
806/// period estimated on the stabilization grid.
807const REFINE_PERIODS: f64 = 5.5;
808/// Grids the shooting with transmission lines converges on: the
809/// first adaptive run's, then one refreshed from the converged orbit.
810const GRID_PASSES: usize = 2;
811/// Largest relative change of `T` in one Newton step.
812const MAX_DT: f64 = 0.2;
813
814/// The last Newton step of an autonomous shooting, for backtracking.
815struct OscStep {
816    /// The base point `(x, T)` …
817    x: Vec<f64>,
818    t: f64,
819    /// … the full step from it …
820    dx: Vec<f64>,
821    dt: f64,
822    /// … the residual at the base, and the fraction of the step taken.
823    base_norm: f64,
824    frac: f64,
825}
826
827/// Where the autonomous shooting starts.
828#[derive(Debug)]
829struct OscStart {
830    /// The rising mid-range crossing of OSCNODE the period starts at.
831    t0: f64,
832    /// The state there (cubic interpolation of the stabilization).
833    x0: Vec<f64>,
834    /// The initial period: the guess, or the crossing estimate.
835    period: f64,
836    note: String,
837}
838
839/// Rising crossings of `level` by unknown `k` over `rows`, linearly
840/// interpolated.
841fn 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
852/// The start of the autonomous shooting from the stabilization rows
853/// (the last `STAB_WINDOW` of TSTAB). The level is the mid-range
854/// `(max + min)/2` of OSCNODE — over the last `EST_PERIODS` periods
855/// once a period is known, so a still-growing amplitude does not bias
856/// it; the start `t0` is the last rising crossing of that level,
857/// where `|dv/dt|` is near its largest (the transversality the phase
858/// condition needs). Without a guess the period is the mean spacing
859/// of the last `EST_PERIODS` + 1 rising crossings — a frequency
860/// counter's estimate, exact for any waveform shape once the orbit is
861/// settled, and its spread is reported.
862fn 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    // First pass: the mid-range of the whole window.
896    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    // Second pass: the mid-range of the last few periods.
926    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    // A measured period beats a guess: FREQ sets the stabilization
936    // grid, and seeds T only when the window has too few crossings.
937    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
971/// The fundamental shared by the source frequencies: their greatest
972/// common divisor, to 1e-9 relative, with at most a million
973/// harmonics up to the highest.
974fn 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
999/// The Newton tolerances of the Jacobian replays.
1000fn 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
1008/// One adaptive period transient and what the shooting reads off it.
1009struct PeriodOut {
1010    rows: Vec<(f64, Vec<f64>)>,
1011    log: Vec<StepRec>,
1012    /// `x(t₀ + T) − x(t₀)`.
1013    resid: Vec<f64>,
1014    /// Per-unknown tolerance (index 0, ground, is 1).
1015    tol: Vec<f64>,
1016    /// The period mismatch `max_i |x_i(t₀+T) − x_i(t₀)| / tol_i`.
1017    rnorm: f64,
1018    /// Lines (issue #20): how far the orbit is from the periodic past
1019    /// it read, `max |x_i(t) − past_i(t)| / tol_i` over the lines' port
1020    /// unknowns (0 without lines).
1021    hnorm: f64,
1022    /// The period of the run.
1023    period: f64,
1024}
1025
1026impl PeriodOut {
1027    /// The residual reported per iteration: the period mismatch, and
1028    /// the lines' history mismatch when there are lines.
1029    fn total(&self) -> f64 {
1030        self.rnorm.max(self.hnorm)
1031    }
1032}
1033
1034/// The periodic past of the transmission lines (issue #20) — the
1035/// device-facing orbit — and the breakpoint rows to interpolate it on
1036/// when checking the next orbit against it.
1037struct Past {
1038    orbit: PeriodicPast,
1039    /// Breakpoint indices into the closed period (0 = the start
1040    /// boundary, 1… the interior samples, `m + 1` the end boundary;
1041    /// only the ends for a past from the stabilization, which has no
1042    /// step log).
1043    breaks: Vec<usize>,
1044}
1045
1046impl Past {
1047    /// The past from `rows` over `[start, start + period]`: its
1048    /// interior samples (boundaries are the shooting state). Samples
1049    /// within `CKTminBreak` of a boundary are left out, as an accept
1050    /// there would be. `log` (a shooting run's) marks the breakpoint
1051    /// rows (`rows[j]` is `log[j − 1]`'s point). `None` when `rows` do
1052    /// not reach back to within a step (`max_step`) of `start`.
1053    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        // The first row may land up to a step after the boundary (the
1062        // boundary sample is the shooting state anyway).
1063        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    /// The device-facing past for a run of period `period`.
1094    fn at(&self, period: f64) -> PeriodicPast {
1095        PeriodicPast {
1096            period,
1097            ..self.orbit.clone()
1098        }
1099    }
1100
1101    /// `max_i |x_i(t) − past_i(t)| / tol_i` over the lines' `ports`
1102    /// at the interior rows of a run from `x` at `t0` on the past's own
1103    /// grid ([`interp`] reproduces a sample exactly at its time).
1104    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
1131/// A frozen step grid (an adaptive period run's log) and the run it
1132/// was taken on, replayable at any period by uniform stretching.
1133struct 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    /// The run and log at period `t`: every time from `t₀` scaled by
1147    /// `t/T` (TSTEP and TMAX too), the end pinned to `t₀ + t`.
1148    fn at(&self, t: f64) -> (PeriodRun, Vec<StepRec>) {
1149        stretch(&self.run, &self.log, t)
1150    }
1151}
1152
1153/// `run`/`log` stretched to period `t` (see [`Grid::at`]); unchanged
1154/// at the run's own period.
1155fn 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    /// GMRES dimension of each Newton step.
1188    krylov: Vec<usize>,
1189    /// Transmission lines (issue #20): the periodic past they read
1190    /// (`None` without lines), their port unknowns, and the history
1191    /// mismatch of every run.
1192    past: Option<Past>,
1193    ports: Vec<usize>,
1194    history: Vec<f64>,
1195    /// The period mismatch of each grid-refresh run.
1196    refresh: Vec<f64>,
1197}
1198
1199/// A fresh engine for one shooting run, with the lines' periodic past
1200/// (if any) at the run's period.
1201fn 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    /// The lines' note: the history mismatch per frozen-grid
1218    /// iteration and the period mismatch of the grid refresh.
1219    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    /// The adaptive transient over the period from `x`.
1245    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    /// What the shooting reads off a period run from `x`: the period
1255    /// mismatch and the per-unknown tolerances.
1256    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    /// Lines (issue #20): one replay of the interval from `x` on the
1289    /// frozen `grid` (an adaptive run's step log, stretched to the
1290    /// current period), the lines reading the orbit `cur` as their
1291    /// periodic past — the Picard half. The replay's rows sit at the
1292    /// times of the past's samples, so the mismatch between the orbit
1293    /// and the history it read is measured exactly. That past stays
1294    /// the shooter's for the Newton half.
1295    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    /// The driven shooting with transmission lines (issue #20). The
1311    /// lines' state is their history, which in the periodic regime is
1312    /// the orbit itself, `x(t − τ) = x(t − τ + kT)`; the shooting
1313    /// unknown stays `x₀` and the history is a fixed point solved
1314    /// alongside. An adaptive run (its past: the stabilization's last
1315    /// period) fixes the grid; on that frozen grid each iteration
1316    /// replays the interval with the previous orbit as the past
1317    /// (Picard) and, while the period mismatch exceeds the tolerance,
1318    /// takes a Newton step on `x₀` with that past frozen (the excited
1319    /// shooting's GMRES). Converged on the grid when both the period
1320    /// mismatch and the history mismatch are within tolerance. The
1321    /// grid is then refreshed once — an adaptive run from the result
1322    /// picks one for the converged orbit — and the iteration converges
1323    /// again on it; that orbit is the result.
1324    ///
1325    /// The frozen grid is what lets the history converge at all.
1326    /// Across grids the lines are not reproducible to the SPICE
1327    /// tolerance: the slope rule plants echo breakpoints on the *sign*
1328    /// of a slope (so two runs place a delayed corner up to a step
1329    /// apart — tens of tolerances at the corner), and the LTRA
1330    /// convolution's error is set by its grid, not by RELTOL (it has no
1331    /// LTE control). The refresh run's period mismatch is that
1332    /// cross-grid discretization, and is reported, not iterated on.
1333    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    /// The autonomous shooting with transmission lines: the scheme of
1378    /// [`Shooter::lines_excited`] with the Newton step of
1379    /// [`Shooter::newton_step_osc`] (`x₀` and `T`; the frozen grid and
1380    /// the past stretch with `T`, both being functions of phase), at
1381    /// least one Newton step (D-0035). Returns the orbit and the last
1382    /// period correction.
1383    #[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    /// The periodic past a converged orbit leaves for the next run.
1449    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    /// The Newton step `δ` of `(I − Φ′) δ = Φ(x) − x`, by GMRES on the
1461    /// scaled unknowns `x_i / tol_i` with `Φ′ v` from frozen-grid
1462    /// replays. The replays first run at tightened Newton tolerances;
1463    /// if one of them fails, the step is redone at the deck's.
1464    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        // Scaled residual, unknowns 1..n.
1491        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    /// The Newton step `(δx, δT)` of an autonomous oscillator: the
1516    /// bordered system `(I − Φ′) δx − (∂Φ/∂T) δT = Φ_T(x) − x` with
1517    /// `δx[osc] = 0` (the phase condition fixes OSCNODE), square
1518    /// again because `T` takes the place of `x[osc]` among the
1519    /// unknowns. GMRES on the scaled unknowns (`x_i / tol_i`,
1520    /// `T / (RELTOL·T)`); one replay per product perturbs `x` and `T`
1521    /// together, `T` by stretching the frozen grid uniformly — the
1522    /// replay is then smooth in `T` as it is in `x` (every step, and
1523    /// every coefficient of the integration formulas, scales with it).
1524    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        // Unknown i sits in slot i − 1 and T in OSCNODE's slot, so
1552        // the operator stays identity plus low rank (the identity's
1553        // osc entry is replaced by the −∂Φ/∂T column), which is what
1554        // makes GMRES converge in a few products.
1555        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            // Lines: the past stretches with the period (it is the
1562            // orbit as a function of phase).
1563            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
1633/// A matrix-free operator `v ↦ A v` (one product may run transients).
1634type 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
1640/// Restart-free GMRES (Saad–Schultz, modified Gram–Schmidt with one
1641/// reorthogonalization, Givens rotations) for `A y = b` from `y = 0`,
1642/// at most `m` iterations, stopping at residual `≤ tol`. Returns the
1643/// solution and the Krylov dimension used.
1644fn 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(); // columns, each of length j + 2
1652    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    // Back substitution on the k×k triangle.
1696    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
1713/// The time-domain and frequency-domain plots of the converged period.
1714fn 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    // The rows at breakpoints (and both ends): a stencil never
1727    // straddles a source corner.
1728    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    // DFT of the first `points` samples (the last one closes the period).
1766    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                // Reduce k·m mod N before the angle: exact arguments.
1782                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
1806/// The solution at `t` by cubic Lagrange interpolation over the four
1807/// accepted points around it, kept inside the breakpoint segment
1808/// (lower order when the segment has fewer points; `breaks` are the
1809/// sorted breakpoint rows, both ends included).
1810fn interp(rows: &[(f64, Vec<f64>)], breaks: &[usize], t: f64) -> Vec<f64> {
1811    // Rows j and j+1 bracket t.
1812    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        // A = I - u wᵀ (a rank-one monodromy): converges in 2 steps.
1855        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}