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        // The shooting's own default method (`Options::pss_method`),
161        // pinned so that every transient below integrates with it.
162        let opts = &Options {
163            method: Some(opts.pss_method()),
164            ..opts.clone()
165        };
166        let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
167        if !(p.tstab.is_finite() && p.tstab >= 0.0) {
168            return bad("stabilization time must be finite and not negative");
169        }
170        if p.points < 4 || p.points > 10_000_000 {
171            return bad("needs 4 to 10 000 000 points per period");
172        }
173        if p.harmonics < 1 || p.harmonics > p.points / 2 {
174            return bad("harmonics must be between 1 and points/2");
175        }
176        if p.max_iter < 1 {
177            return bad("needs at least one shooting iteration");
178        }
179        if !(p.steady_coeff.is_finite() && p.steady_coeff > 0.0) {
180            return bad("steady coefficient must be positive");
181        }
182        if opts.dev_noise {
183            // Issue #22: the same reason as the TRNOISE/TRRANDOM
184            // sources below. The option used to be ignored here
185            // (only `.tran` installs the generators), so the result
186            // looked as if it had taken the noise into account.
187            return Err(Error::Build(
188                ".pss: `.options devnoise` is on, and transient device noise is a random \
189                 stimulus, which has no periodic steady state; turn it off for the shooting"
190                    .into(),
191            ));
192        }
193        for spec in &self.specs {
194            let noisy = match spec {
195                Spec::Vsource(v) => &v.wave,
196                Spec::Isource(i) => &i.wave,
197                _ => &None,
198            };
199            if matches!(noisy, Some(Wave::TrNoise(_) | Wave::TrRandom(_))) {
200                // Issue #13, part 2: a random stimulus has no periodic
201                // steady state; shooting would chase a moving target.
202                return Err(Error::Unsupported {
203                    name: spec.name().to_string(),
204                    what: "a transient-noise source (TRNOISE/TRRANDOM) in .pss — a random \
205                           stimulus has no periodic steady state"
206                        .into(),
207                });
208            }
209            if let Spec::Ltra(l) = spec
210                && !opts.ltra_fit
211            {
212                // Issue #20: the periodic past of a lossy line is
213                // infinite; only the recursive model sums it.
214                let lp = crate::ltra::LtraParams::new(&l.name, &l.model, opts.reltol, opts.abstol)
215                    .map_err(Error::Build)?;
216                if matches!(
217                    lp.case,
218                    crate::ltra::LtraCase::Rlc | crate::ltra::LtraCase::Rc
219                ) {
220                    return Err(Error::Build(format!(
221                        ".pss: the lossy (RLC/RC) line `{}` needs `.options ltrafit`: in the \
222                         periodic regime its history is the orbit repeated over the whole past, \
223                         which the exact convolution (a walk over a history that starts at an \
224                         operating point) cannot hold, and the recursive model sums in closed \
225                         form",
226                        l.name
227                    )));
228                }
229            }
230        }
231        let mut notes = Vec::new();
232        let sources = self.source_frequencies()?;
233        if sources.is_empty() {
234            // No periodic stimulus: a free-running oscillator, its
235            // period an unknown of the shooting (see `pss_autonomous`).
236            return self.pss_autonomous(opts, p);
237        }
238        let freq = match p.freq {
239            Some(f) => {
240                if !(f.is_finite() && f > 0.0) {
241                    return bad("frequency must be positive");
242                }
243                for (name, fs) in &sources {
244                    let k = fs / f;
245                    if (k - k.round()).abs() > 1e-6 * k.max(1.0) || k.round() < 1.0 {
246                        notes.push(format!(
247                            "pss: source `{name}` ({fs:e} Hz) is not a harmonic of the \
248                             fundamental {f:e} Hz; the circuit is not {:e}-s periodic",
249                            1.0 / f
250                        ));
251                    }
252                }
253                f
254            }
255            None => harvest(&sources)?,
256        };
257        let period = 1.0 / freq;
258        let tstep = period / p.points as f64;
259        let max_step = f64::min(tstep, period / 50.0);
260        let n_stab = (p.tstab / period - 1e-9).ceil().max(1.0);
261        if n_stab > 1e9 {
262            return bad("stabilization spans too many periods");
263        }
264        let t0 = n_stab * period;
265        let run = PeriodRun {
266            t0,
267            period,
268            tstep,
269            max_step,
270        };
271
272        // Stabilization: a plain transient to t0.
273        let built = self.build(opts)?;
274        notes.extend(built.notes.iter().cloned());
275        let mut engine = Engine::new(built);
276        // Work guards (hostile cards must cost nothing, CLAUDE.md
277        // princípio 4): a period transient keeps about `points` rows,
278        // the stabilization takes `n_stab · points` steps, the DFT
279        // `harmonics · points` products per unknown.
280        let nvars = engine.vars.len() as f64;
281        let pts = p.points as f64;
282        if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
283            || n_stab * pts > 1e8
284            || p.harmonics as f64 * pts * nvars > 1e9
285        {
286            return bad("asks for too many points or periods; refusing");
287        }
288        if let Some(node) = &p.osc_node {
289            let lc = node.to_ascii_lowercase();
290            if !engine.vars.names.contains(&lc) {
291                return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
292            }
293            notes.push(format!(
294                "pss: oscnode `{lc}` is for autonomous circuits; the driven shooting does not use it"
295            ));
296        }
297        let ports = self.line_ports(&engine.vars.names);
298        let lines = !ports.is_empty();
299        // With lines, the last stabilization period is the first guess
300        // of the periodic past (only the rows kept change).
301        let stab = engine.tran(
302            opts,
303            &TranParams {
304                tstep,
305                tstop: t0,
306                tstart: if lines { t0 - period } else { t0 },
307                tmax: Some(max_step),
308                uic: p.uic,
309            },
310        )?;
311        let names = engine.vars.names.clone();
312        let kinds = engine.vars.kinds.clone();
313        let mut x = engine.sol.clone();
314        drop(engine);
315
316        let mut shooter = Shooter {
317            circuit: self,
318            opts,
319            tight: tightened(opts),
320            run,
321            kinds: &kinds,
322            adaptive: 0,
323            replays: 0,
324            krylov: Vec::new(),
325            past: if lines {
326                Some(
327                    Past::from_rows(&stab, None, t0 - period, period, max_step)
328                        .ok_or_else(|| Error::Build(".pss: no stabilization period".into()))?,
329                )
330            } else {
331                None
332            },
333            ports,
334            history: Vec::new(),
335            refresh: Vec::new(),
336        };
337        let mut residuals = Vec::new();
338        let converged = if lines {
339            shooter.lines_excited(&mut x, p, &mut residuals)?
340        } else {
341            // The last Newton step taken: (base point, step, base residual, fraction).
342            let mut last: Option<(Vec<f64>, Vec<f64>, f64, f64)> = None;
343            let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
344            loop {
345                // A transient that fails from a Newton iterate (an
346                // overshoot into a region the step control cannot cross)
347                // counts as an infinite residual and is backtracked.
348                let (mut period_run, rnorm) = match shooter.adaptive(&x) {
349                    Ok(r) => {
350                        let n = r.rnorm;
351                        (Some(r), n)
352                    }
353                    Err(e) => match &last {
354                        Some((_, _, _, frac)) if *frac > min_frac => (None, f64::INFINITY),
355                        _ => return Err(e),
356                    },
357                };
358                residuals.push(rnorm);
359                if rnorm <= p.steady_coeff
360                    && let Some(r) = period_run.take()
361                {
362                    break r;
363                }
364                if residuals.len() >= p.max_iter {
365                    return Err(Error::PssConvergence {
366                        iterations: residuals.len(),
367                        residuals,
368                        period: None,
369                    });
370                }
371                // Backtrack a Newton step that did not reduce the residual.
372                if let Some((base, step, base_norm, frac)) = &mut last
373                    && rnorm > *base_norm
374                    && *frac > min_frac
375                {
376                    *frac *= 0.5;
377                    for i in 0..x.len() {
378                        x[i] = base[i] + *frac * step[i];
379                    }
380                    continue;
381                }
382                let Some(period_run) = period_run else {
383                    unreachable!("a failed run is always backtracked")
384                };
385                let step = shooter.newton_step(&x, &period_run)?;
386                let base = x.clone();
387                for i in 1..x.len() {
388                    x[i] += step[i];
389                }
390                last = Some((base, step, rnorm, 1.0));
391            }
392        };
393
394        notes.push(format!(
395            "pss: period {period:e} s (fundamental {freq:e} Hz{}); shooting over \
396             [{t0:e}, {:e}] s after {n_stab} stabilization period(s)",
397            if p.freq.is_some() {
398                String::new()
399            } else {
400                ", from the sources".to_string()
401            },
402            t0 + period
403        ));
404        notes.push(shooter.summary(&residuals));
405        notes.extend(shooter.lines_note());
406
407        let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
408        let periods = vec![period; residuals.len()];
409        Ok(Pss {
410            time,
411            spectrum,
412            period,
413            t0,
414            residuals,
415            periods,
416            notes,
417        })
418    }
419
420    /// The port unknowns of the transmission lines (issue #20) — the
421    /// four terminals and the two port-current equations of every
422    /// T/O line, ground left out —, whose orbit must reproduce the
423    /// periodic past the lines read.
424    fn line_ports(&self, names: &[String]) -> Vec<usize> {
425        let find = |n: &str| names.iter().position(|v| v.eq_ignore_ascii_case(n));
426        let mut out = Vec::new();
427        for spec in &self.specs {
428            let (name, nodes) = match spec {
429                Spec::Ltra(l) => (&l.name, &l.nodes),
430                Spec::Tra(t) => (&t.name, &t.nodes),
431                _ => continue,
432            };
433            let branches = [format!("{name}#i1"), format!("{name}#i2")];
434            for n in nodes
435                .iter()
436                .map(String::as_str)
437                .chain(branches.iter().map(String::as_str))
438            {
439                if let Some(i) = find(n)
440                    && i != 0
441                    && !out.contains(&i)
442                {
443                    out.push(i);
444                }
445            }
446        }
447        out
448    }
449
450    /// The frequencies the independent sources repeat at: SIN and
451    /// SFFM carriers/modulations, PULSE `1/PER`. Waveforms that settle
452    /// to a constant (EXP, PWL, a PULSE without PER) set none. A SIN
453    /// or SFFM without an explicit frequency (its default is
454    /// `1/TSTOP`) or a damped SIN has no period and is an error.
455    fn source_frequencies(&self) -> Result<Vec<(String, f64)>, Error> {
456        let mut out = Vec::new();
457        for spec in &self.specs {
458            let (name, wave) = match spec {
459                Spec::Vsource(v) => (&v.name, &v.wave),
460                Spec::Isource(i) => (&i.name, &i.wave),
461                _ => continue,
462            };
463            let Some(wave) = wave else { continue };
464            let given = |c: &[f64], i: usize| c.get(i).copied().filter(|&v| v != 0.0);
465            let no_period = |what: &str| {
466                Err(Error::Build(format!(
467                    ".pss: source `{name}`: {what} has no fixed period"
468                )))
469            };
470            match wave {
471                Wave::Sin(c) => {
472                    if given(c, 1).is_none() {
473                        continue;
474                    }
475                    if given(c, 4).is_some() {
476                        return no_period("a damped SIN");
477                    }
478                    match given(c, 2) {
479                        Some(f) => out.push((name.clone(), f.abs())),
480                        None => return no_period("a SIN without a frequency"),
481                    }
482                }
483                Wave::Sffm(c) => {
484                    if given(c, 1).is_none() {
485                        continue;
486                    }
487                    match given(c, 2) {
488                        Some(f) => out.push((name.clone(), f.abs())),
489                        None => return no_period("an SFFM without a carrier frequency"),
490                    }
491                    if given(c, 3).is_some() {
492                        match given(c, 4) {
493                            Some(f) => out.push((name.clone(), f.abs())),
494                            None => return no_period("an SFFM without a signal frequency"),
495                        }
496                    }
497                }
498                Wave::Pulse(c) => {
499                    let v1 = c.first().copied().unwrap_or(0.0);
500                    let v2 = c.get(1).copied().unwrap_or(0.0);
501                    if v1 != v2
502                        && let Some(per) = given(c, 6)
503                    {
504                        out.push((name.clone(), 1.0 / per.abs()));
505                    }
506                }
507                Wave::Exp(_) | Wave::Pwl(_) => {}
508                // Refused by `Circuit::pss` before the harvest.
509                Wave::TrNoise(_) | Wave::TrRandom(_) => {}
510            }
511        }
512        Ok(out)
513    }
514}
515
516impl Circuit {
517    /// `.pss` of an autonomous oscillator (no periodic source): the
518    /// period is an unknown. Shooting–Newton on the augmented system
519    /// `[Φ_T(x₀) − x₀ = 0; x₀[osc] = v_ref]`, the phase condition
520    /// fixing `OSCNODE` at its value on a rising crossing of its
521    /// mid-range (see [`osc_start`]); the bordered Jacobian
522    /// `[I − Φ′, −∂Φ/∂T]` (osc column dropped) is solved by the same
523    /// matrix-free GMRES, `∂Φ/∂T` by a replay on the frozen grid
524    /// stretched to `T + δT`.
525    fn pss_autonomous(&self, opts: &Options, p: &PssParams) -> Result<Pss, Error> {
526        let bad = |m: &str| Err(Error::Build(format!(".pss {m}")));
527        let guess = match p.freq {
528            Some(f) if !(f.is_finite() && f > 0.0) => return bad("frequency must be positive"),
529            Some(f) => Some(1.0 / f),
530            None => None,
531        };
532        let Some(node) = &p.osc_node else {
533            return bad(
534                "of an autonomous circuit (no periodic source) needs the oscillation node \
535                 OSCNODE",
536            );
537        };
538        if p.tstab <= 0.0 {
539            return bad(
540                "of an autonomous circuit needs a stabilization time TSTAB: the oscillation \
541                 must start and be measured before the shooting",
542            );
543        }
544        let built = self.build(opts)?;
545        let mut notes: Vec<String> = built.notes.clone();
546        let mut engine = Engine::new(built);
547        let lc = node.to_ascii_lowercase();
548        let Some(osc) = engine.vars.names.iter().position(|n| *n == lc) else {
549            return Err(Error::Build(format!(".pss: no node `{node}` for oscnode")));
550        };
551        if osc == 0 || engine.vars.kinds[osc] != VarKind::Node {
552            return Err(Error::Build(format!(
553                ".pss: oscnode `{node}` must be a circuit node"
554            )));
555        }
556        // Stabilization grid: with a guess, the driven one; without,
557        // TMAX = TSTAB/STAB_POINTS (the LTE control refines it).
558        let (tstep, max_step) = match guess {
559            Some(t) => (t / p.points as f64, f64::min(t / p.points as f64, t / 50.0)),
560            None => (p.tstab / STAB_POINTS, p.tstab / STAB_POINTS),
561        };
562        let nvars = engine.vars.len() as f64;
563        let pts = p.points as f64;
564        if (pts + 1.0) * nvars > MAX_OUTPUT_VALUES as f64
565            || p.tstab / tstep > 1e8
566            || p.harmonics as f64 * pts * nvars > 1e9
567        {
568            return bad("asks for too many points or periods; refusing");
569        }
570        let rows = engine.tran(
571            opts,
572            &TranParams {
573                tstep,
574                tstop: p.tstab,
575                tstart: p.tstab * (1.0 - STAB_WINDOW),
576                tmax: Some(max_step),
577                uic: p.uic,
578            },
579        )?;
580        let names = engine.vars.names.clone();
581        let kinds = engine.vars.kinds.clone();
582        let ports = self.line_ports(&names);
583        drop(engine);
584        // Lines: a continuation from the stabilization (the refine
585        // run, and the shooting) reads the last period before it as
586        // its periodic past.
587        let no_past = || {
588            Error::Build(
589                ".pss: the stabilization window holds less than one period before the \
590                 shooting start; lengthen TSTAB"
591                    .into(),
592            )
593        };
594        let past_before = |rows: &[(f64, Vec<f64>)], at: f64, t: f64, max_step: f64| {
595            if ports.is_empty() {
596                Ok(None)
597            } else {
598                Past::from_rows(rows, None, at - t, t, max_step)
599                    .map(Some)
600                    .ok_or_else(no_past)
601            }
602        };
603        let mut start = osc_start(&rows, osc, &lc, guess, opts)?;
604        let mut start_rows = rows;
605        if guess.is_none() {
606            // The stabilization grid knew no period: refine the
607            // estimate (and the start) on a short continuation at
608            // the shooting's own grid, T/POINTS, from where it ended.
609            let (t_end, x_end) = start_rows.last().expect("the stabilization has rows");
610            let t = start.period;
611            let tstep = t / p.points as f64;
612            let refine = PeriodRun {
613                t0: *t_end,
614                period: REFINE_PERIODS * t,
615                tstep,
616                max_step: f64::min(tstep, t / 50.0),
617            };
618            let past = past_before(&start_rows, *t_end, t, max_step)?;
619            let mut engine = Engine::new(self.build(opts)?);
620            engine.set_periodic_past(past.as_ref().map(|p| p.at(t)));
621            let fine = engine.tran_period(opts, &refine, x_end, &mut Vec::new())?;
622            notes.push(start.note);
623            start = osc_start(&fine, osc, &lc, None, opts)?;
624            start_rows = fine;
625            start.note = format!(
626                "{} (refined over {REFINE_PERIODS} periods continued from TSTAB at T/POINTS)",
627                start.note
628            );
629        }
630        notes.push(start.note.clone());
631        let t0 = start.t0;
632        let mut x = start.x0;
633        let mut period = start.period;
634
635        let run_for = |t: f64| PeriodRun {
636            t0,
637            period: t,
638            tstep: t / p.points as f64,
639            max_step: f64::min(t / p.points as f64, t / 50.0),
640        };
641        let first_past = past_before(&start_rows, t0, period, run_for(period).max_step)?;
642        drop(start_rows);
643        let mut shooter = Shooter {
644            circuit: self,
645            opts,
646            tight: tightened(opts),
647            run: run_for(period),
648            kinds: &kinds,
649            adaptive: 0,
650            replays: 0,
651            krylov: Vec::new(),
652            past: first_past,
653            ports: ports.clone(),
654            history: Vec::new(),
655            refresh: Vec::new(),
656        };
657        let mut residuals = Vec::new();
658        let mut periods = Vec::new();
659        let mut last: Option<OscStep> = None;
660        let min_frac = 0.5f64.powi(MAX_BACKTRACK as i32);
661        let fail = |residuals: Vec<f64>, period: f64| Error::PssConvergence {
662            iterations: residuals.len(),
663            residuals,
664            period: Some(period),
665        };
666        let converged = if shooter.past.is_some() {
667            let (r, dt) = shooter.lines_autonomous(
668                &mut x,
669                &mut period,
670                osc,
671                p,
672                &run_for,
673                &mut residuals,
674                &mut periods,
675            )?;
676            last = Some(OscStep {
677                x: Vec::new(),
678                t: period,
679                dx: Vec::new(),
680                dt,
681                base_norm: 0.0,
682                frac: 1.0,
683            });
684            r
685        } else {
686            loop {
687                shooter.run = run_for(period);
688                let (mut period_run, rnorm) = match shooter.adaptive(&x) {
689                    Ok(r) => {
690                        let n = r.rnorm;
691                        (Some(r), n)
692                    }
693                    Err(e) => match &last {
694                        Some(l) if l.frac > min_frac => (None, f64::INFINITY),
695                        Some(_) => {
696                            residuals.push(f64::INFINITY);
697                            periods.push(period);
698                            return Err(fail(residuals, period));
699                        }
700                        None => return Err(e),
701                    },
702                };
703                residuals.push(rnorm);
704                periods.push(period);
705                // At least one Newton step: an estimate already within
706                // the tolerance still gets its T corrected (quadratically)
707                // — the mismatch criterion alone admits a period error of
708                // about tol/|dx/dt| — and the step's δT is reported.
709                if rnorm <= p.steady_coeff
710                    && last.is_some()
711                    && let Some(r) = period_run.take()
712                {
713                    break r;
714                }
715                if residuals.len() >= p.max_iter {
716                    return Err(fail(residuals, period));
717                }
718                if let Some(l) = &mut last
719                    && rnorm > l.base_norm
720                    && l.frac > min_frac
721                {
722                    l.frac *= 0.5;
723                    for ((xi, bx), sx) in x.iter_mut().zip(&l.x).zip(&l.dx) {
724                        *xi = bx + l.frac * sx;
725                    }
726                    period = l.t + l.frac * l.dt;
727                    continue;
728                }
729                let Some(period_run) = period_run else {
730                    unreachable!("a failed run is always backtracked")
731                };
732                let (mut sx, mut st) = shooter.newton_step_osc(&x, period, osc, &period_run)?;
733                // A Newton step never moves T by more than MAX_DT of itself
734                // (far from the orbit the linearized phase drift is not
735                // to be trusted); the whole step is scaled.
736                if st.abs() > MAX_DT * period {
737                    let k = MAX_DT * period / st.abs();
738                    sx.iter_mut().for_each(|v| *v *= k);
739                    st *= k;
740                }
741                let base = x.clone();
742                for i in 1..x.len() {
743                    x[i] += sx[i];
744                }
745                let bt = period;
746                period += st;
747                last = Some(OscStep {
748                    x: base,
749                    t: bt,
750                    dx: sx,
751                    dt: st,
752                    base_norm: rnorm,
753                    frac: 1.0,
754                });
755            }
756        };
757
758        // The trivial periodic solution — the equilibrium — is
759        // periodic for every T; a converged "orbit" with no swing at
760        // OSCNODE is that, not an oscillation.
761        let (lo, hi) = converged
762            .rows
763            .iter()
764            .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
765                (a.min(r.1[osc]), b.max(r.1[osc]))
766            });
767        if hi - lo <= 10.0 * converged.tol[osc] {
768            return Err(Error::Build(format!(
769                ".pss: the shooting settled on the equilibrium (v({lc}) swings {:e} V); \
770                 the circuit does not oscillate",
771                hi - lo
772            )));
773        }
774        let hist: Vec<String> = residuals
775            .iter()
776            .zip(&periods)
777            .map(|(r, t)| format!("[{r:.3e}, {t:.9e}]"))
778            .collect();
779        let dt = last.as_ref().map_or(0.0, |l| l.dt * l.frac);
780        notes.push(format!(
781            "pss: autonomous oscillator: period {period:e} s (frequency {:e} Hz) is the \
782             shooting's result (last Newton correction {dt:.3e} s); [residual/tolerance, T] \
783             per iteration: {}; phase condition v({lc}) = {:e} V at t0 = {t0:e} s",
784            1.0 / period,
785            hist.join(", "),
786            x[osc]
787        ));
788        notes.push(shooter.summary(&residuals));
789        notes.extend(shooter.lines_note());
790        let (time, spectrum) = outputs(&converged, &names, &kinds, p, t0, period);
791        Ok(Pss {
792            time,
793            spectrum,
794            period,
795            t0,
796            residuals,
797            periods,
798            notes,
799        })
800    }
801}
802
803/// Default stabilization grid of an autonomous `.pss` without a
804/// frequency guess: TMAX = TSTAB / this.
805const STAB_POINTS: f64 = 1000.0;
806/// The fraction of TSTAB (its end) the stabilization keeps and
807/// measures the oscillation on.
808const STAB_WINDOW: f64 = 0.5;
809/// Rising crossings the period estimate averages over (at most).
810const EST_PERIODS: usize = 4;
811/// Length, in estimated periods, of the continuation that refines a
812/// period estimated on the stabilization grid.
813const REFINE_PERIODS: f64 = 5.5;
814/// Grids the shooting with transmission lines converges on: the
815/// first adaptive run's, then one refreshed from the converged orbit.
816const GRID_PASSES: usize = 2;
817/// Largest relative change of `T` in one Newton step.
818const MAX_DT: f64 = 0.2;
819
820/// The last Newton step of an autonomous shooting, for backtracking.
821struct OscStep {
822    /// The base point `(x, T)` …
823    x: Vec<f64>,
824    t: f64,
825    /// … the full step from it …
826    dx: Vec<f64>,
827    dt: f64,
828    /// … the residual at the base, and the fraction of the step taken.
829    base_norm: f64,
830    frac: f64,
831}
832
833/// Where the autonomous shooting starts.
834#[derive(Debug)]
835struct OscStart {
836    /// The rising mid-range crossing of OSCNODE the period starts at.
837    t0: f64,
838    /// The state there (cubic interpolation of the stabilization).
839    x0: Vec<f64>,
840    /// The initial period: the guess, or the crossing estimate.
841    period: f64,
842    note: String,
843}
844
845/// Rising crossings of `level` by unknown `k` over `rows`, linearly
846/// interpolated.
847fn rising_crossings(rows: &[(f64, Vec<f64>)], k: usize, level: f64) -> Vec<f64> {
848    let mut out = Vec::new();
849    for w in rows.windows(2) {
850        let (a, b) = (w[0].1[k], w[1].1[k]);
851        if a < level && b >= level {
852            out.push(w[0].0 + (w[1].0 - w[0].0) * (level - a) / (b - a));
853        }
854    }
855    out
856}
857
858/// The start of the autonomous shooting from the stabilization rows
859/// (the last `STAB_WINDOW` of TSTAB). The level is the mid-range
860/// `(max + min)/2` of OSCNODE — over the last `EST_PERIODS` periods
861/// once a period is known, so a still-growing amplitude does not bias
862/// it; the start `t0` is the last rising crossing of that level,
863/// where `|dv/dt|` is near its largest (the transversality the phase
864/// condition needs). Without a guess the period is the mean spacing
865/// of the last `EST_PERIODS` + 1 rising crossings — a frequency
866/// counter's estimate, exact for any waveform shape once the orbit is
867/// settled, and its spread is reported.
868fn osc_start(
869    rows: &[(f64, Vec<f64>)],
870    osc: usize,
871    name: &str,
872    guess: Option<f64>,
873    opts: &Options,
874) -> Result<OscStart, Error> {
875    let span = |from: f64| {
876        rows.iter()
877            .filter(|r| r.0 >= from)
878            .fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), r| {
879                (a.min(r.1[osc]), b.max(r.1[osc]))
880            })
881    };
882    let (Some(first), Some(end)) = (rows.first(), rows.last()) else {
883        return Err(Error::Build(".pss: empty stabilization".into()));
884    };
885    let (t_first, t_end) = (first.0, end.0);
886    let (lo, hi) = span(t_first);
887    let floor = 10.0 * (opts.reltol * lo.abs().max(hi.abs()) + opts.vntol);
888    let quiet = || {
889        Err(Error::Build(format!(
890            ".pss: the circuit does not oscillate at the oscillation node within TSTAB \
891             ({:e} V of swing over the last {:.0}% of it); kick it out of its equilibrium \
892             (an initial condition with UIC, or a supply ramp by PWL/PULSE) or lengthen \
893             TSTAB",
894            hi - lo,
895            100.0 * STAB_WINDOW
896        )))
897    };
898    if (hi - lo).is_nan() || hi - lo <= floor {
899        return quiet();
900    }
901    // First pass: the mid-range of the whole window.
902    let c = rising_crossings(rows, osc, 0.5 * (lo + hi));
903    let estimate = |c: &[f64]| -> Option<(f64, f64)> {
904        if c.len() < 3 {
905            return None;
906        }
907        let k = EST_PERIODS.min(c.len() - 1);
908        let tail = &c[c.len() - 1 - k..];
909        let mean = (tail[k] - tail[0]) / k as f64;
910        let spread = tail
911            .windows(2)
912            .map(|w| ((w[1] - w[0]) / mean - 1.0).abs())
913            .fold(0.0, f64::max);
914        Some((mean, spread))
915    };
916    let rough = match guess {
917        Some(t) => t,
918        None => match estimate(&c) {
919            Some((t, _)) => t,
920            None => {
921                return Err(Error::Build(format!(
922                    ".pss: {} rising crossing(s) of the oscillation node's mid-range in the \
923                     last {:.0}% of TSTAB; estimating the period needs 3 — lengthen TSTAB or \
924                     give FREQ",
925                    c.len(),
926                    100.0 * STAB_WINDOW
927                )));
928            }
929        },
930    };
931    // Second pass: the mid-range of the last few periods.
932    let (lo2, hi2) = span(f64::max(t_first, t_end - EST_PERIODS as f64 * rough));
933    if (hi2 - lo2).is_nan() || hi2 - lo2 <= floor {
934        return quiet();
935    }
936    let level = 0.5 * (lo2 + hi2);
937    let c = rising_crossings(rows, osc, level);
938    let Some(&t0) = c.last() else {
939        return quiet();
940    };
941    // A measured period beats a guess: FREQ sets the stabilization
942    // grid, and seeds T only when the window has too few crossings.
943    let guessed = guess.map_or_else(String::new, |g| format!("; FREQ's guess was {g:e} s"));
944    let (period, how) = match (estimate(&c), guess) {
945        (Some((t, spread)), _) => {
946            let k = EST_PERIODS.min(c.len() - 1);
947            (
948                t,
949                format!(
950                    "estimate {t:e} s from the last {k} periods between rising crossings \
951                     (spread {spread:.1e}){guessed}"
952                ),
953            )
954        }
955        (None, Some(g)) => (
956            g,
957            format!("guess {g:e} s from FREQ (too few crossings to measure one)"),
958        ),
959        (None, None) => (rough, format!("estimate {rough:e} s (first pass)")),
960    };
961    let all = [0, rows.len() - 1];
962    let x0 = interp(rows, &all, t0);
963    Ok(OscStart {
964        t0,
965        x0,
966        period,
967        note: format!(
968            "pss: no periodic source — autonomous oscillator on v({}); initial period {how}; \
969             mid-range level {level:e} V, swing {:e} V; shooting starts at its last rising \
970             crossing, t0 = {t0:e} s",
971            name,
972            hi2 - lo2
973        ),
974    })
975}
976
977/// The fundamental shared by the source frequencies: their greatest
978/// common divisor, to 1e-9 relative, with at most a million
979/// harmonics up to the highest.
980fn harvest(sources: &[(String, f64)]) -> Result<f64, Error> {
981    let Some(fmax) = sources.iter().map(|s| s.1).reduce(f64::max) else {
982        return Err(Error::Build(
983            ".pss: no periodic source; give the fundamental frequency".into(),
984        ));
985    };
986    let tol = 1e-9 * fmax;
987    let gcd = |a: f64, b: f64| {
988        let (mut a, mut b) = (a.max(b), a.min(b));
989        while b > tol {
990            let r = (a - b * (a / b).round()).abs();
991            a = b;
992            b = if r <= tol { 0.0 } else { r };
993        }
994        a
995    };
996    let f = sources.iter().map(|s| s.1).fold(0.0, gcd);
997    if f.is_nan() || f <= 0.0 || fmax / f > 1e6 {
998        return Err(Error::Build(
999            ".pss: the source frequencies share no fundamental; give it".into(),
1000        ));
1001    }
1002    Ok(f)
1003}
1004
1005/// The Newton tolerances of the Jacobian replays.
1006fn tightened(opts: &Options) -> Options {
1007    let mut o = opts.clone();
1008    o.reltol *= REPLAY_TIGHTEN;
1009    o.vntol *= REPLAY_TIGHTEN;
1010    o.abstol *= REPLAY_TIGHTEN;
1011    o
1012}
1013
1014/// One adaptive period transient and what the shooting reads off it.
1015struct PeriodOut {
1016    rows: Vec<(f64, Vec<f64>)>,
1017    log: Vec<StepRec>,
1018    /// `x(t₀ + T) − x(t₀)`.
1019    resid: Vec<f64>,
1020    /// Per-unknown tolerance (index 0, ground, is 1).
1021    tol: Vec<f64>,
1022    /// The period mismatch `max_i |x_i(t₀+T) − x_i(t₀)| / tol_i`.
1023    rnorm: f64,
1024    /// Lines (issue #20): how far the orbit is from the periodic past
1025    /// it read, `max |x_i(t) − past_i(t)| / tol_i` over the lines' port
1026    /// unknowns (0 without lines).
1027    hnorm: f64,
1028    /// The period of the run.
1029    period: f64,
1030}
1031
1032impl PeriodOut {
1033    /// The residual reported per iteration: the period mismatch, and
1034    /// the lines' history mismatch when there are lines.
1035    fn total(&self) -> f64 {
1036        self.rnorm.max(self.hnorm)
1037    }
1038}
1039
1040/// The periodic past of the transmission lines (issue #20) — the
1041/// device-facing orbit — and the breakpoint rows to interpolate it on
1042/// when checking the next orbit against it.
1043struct Past {
1044    orbit: PeriodicPast,
1045    /// Breakpoint indices into the closed period (0 = the start
1046    /// boundary, 1… the interior samples, `m + 1` the end boundary;
1047    /// only the ends for a past from the stabilization, which has no
1048    /// step log).
1049    breaks: Vec<usize>,
1050}
1051
1052impl Past {
1053    /// The past from `rows` over `[start, start + period]`: its
1054    /// interior samples (boundaries are the shooting state). Samples
1055    /// within `CKTminBreak` of a boundary are left out, as an accept
1056    /// there would be. `log` (a shooting run's) marks the breakpoint
1057    /// rows (`rows[j]` is `log[j − 1]`'s point). `None` when `rows` do
1058    /// not reach back to within a step (`max_step`) of `start`.
1059    fn from_rows(
1060        rows: &[(f64, Vec<f64>)],
1061        log: Option<&[StepRec]>,
1062        start: f64,
1063        period: f64,
1064        max_step: f64,
1065    ) -> Option<Past> {
1066        let mb = 5e-5 * max_step;
1067        // The first row may land up to a step after the boundary (the
1068        // boundary sample is the shooting state anyway).
1069        if rows.first().is_none_or(|r| r.0 > start + max_step + mb) {
1070            return None;
1071        }
1072        let mut phase = Vec::new();
1073        let mut kept = Vec::new();
1074        let mut breaks = vec![0usize];
1075        for (j, (t, x)) in rows.iter().enumerate() {
1076            if *t - start <= mb || start + period - *t <= mb {
1077                continue;
1078            }
1079            phase.push((*t - start) / period);
1080            kept.push(x.clone());
1081            if let Some(log) = log
1082                && j >= 1
1083                && log.get(j - 1).is_some_and(|r| r.brk)
1084            {
1085                breaks.push(phase.len());
1086            }
1087        }
1088        breaks.push(phase.len() + 1);
1089        Some(Past {
1090            orbit: PeriodicPast {
1091                period,
1092                phase,
1093                rows: kept,
1094            },
1095            breaks,
1096        })
1097    }
1098
1099    /// The device-facing past for a run of period `period`.
1100    fn at(&self, period: f64) -> PeriodicPast {
1101        PeriodicPast {
1102            period,
1103            ..self.orbit.clone()
1104        }
1105    }
1106
1107    /// `max_i |x_i(t) − past_i(t)| / tol_i` over the lines' `ports`
1108    /// at the interior rows of a run from `x` at `t0` on the past's own
1109    /// grid ([`interp`] reproduces a sample exactly at its time).
1110    fn mismatch(
1111        &self,
1112        rows: &[(f64, Vec<f64>)],
1113        x: &[f64],
1114        t0: f64,
1115        period: f64,
1116        ports: &[usize],
1117        tol: &[f64],
1118    ) -> f64 {
1119        let mut closed: Vec<(f64, Vec<f64>)> = Vec::with_capacity(self.orbit.phase.len() + 2);
1120        closed.push((0.0, x.to_vec()));
1121        for (u, r) in self.orbit.phase.iter().zip(&self.orbit.rows) {
1122            closed.push((u * period, r.clone()));
1123        }
1124        closed.push((period, x.to_vec()));
1125        let mut worst = 0.0f64;
1126        let n = rows.len();
1127        for (t, sol) in rows.iter().take(n.saturating_sub(1)).skip(1) {
1128            let v = interp(&closed, &self.breaks, t - t0);
1129            for &i in ports {
1130                worst = worst.max((sol[i] - v[i]).abs() / tol[i]);
1131            }
1132        }
1133        worst
1134    }
1135}
1136
1137/// A frozen step grid (an adaptive period run's log) and the run it
1138/// was taken on, replayable at any period by uniform stretching.
1139struct Grid {
1140    run: PeriodRun,
1141    log: Vec<StepRec>,
1142}
1143
1144impl Grid {
1145    fn of(run: PeriodRun, log: &[StepRec]) -> Grid {
1146        Grid {
1147            run,
1148            log: log.to_vec(),
1149        }
1150    }
1151
1152    /// The run and log at period `t`: every time from `t₀` scaled by
1153    /// `t/T` (TSTEP and TMAX too), the end pinned to `t₀ + t`.
1154    fn at(&self, t: f64) -> (PeriodRun, Vec<StepRec>) {
1155        stretch(&self.run, &self.log, t)
1156    }
1157}
1158
1159/// `run`/`log` stretched to period `t` (see [`Grid::at`]); unchanged
1160/// at the run's own period.
1161fn stretch(run: &PeriodRun, log: &[StepRec], t: f64) -> (PeriodRun, Vec<StepRec>) {
1162    if t == run.period {
1163        return (*run, log.to_vec());
1164    }
1165    let k = t / run.period;
1166    let stretched = PeriodRun {
1167        t0: run.t0,
1168        period: t,
1169        tstep: run.tstep * k,
1170        max_step: run.max_step * k,
1171    };
1172    let mut log: Vec<StepRec> = log
1173        .iter()
1174        .map(|r| StepRec {
1175            time: run.t0 + (r.time - run.t0) * k,
1176            ..*r
1177        })
1178        .collect();
1179    if let Some(end) = log.last_mut() {
1180        end.time = run.t0 + t;
1181    }
1182    (stretched, log)
1183}
1184
1185struct Shooter<'a> {
1186    circuit: &'a Circuit,
1187    opts: &'a Options,
1188    tight: Options,
1189    run: PeriodRun,
1190    kinds: &'a [VarKind],
1191    adaptive: usize,
1192    replays: usize,
1193    /// GMRES dimension of each Newton step.
1194    krylov: Vec<usize>,
1195    /// Transmission lines (issue #20): the periodic past they read
1196    /// (`None` without lines), their port unknowns, and the history
1197    /// mismatch of every run.
1198    past: Option<Past>,
1199    ports: Vec<usize>,
1200    history: Vec<f64>,
1201    /// The period mismatch of each grid-refresh run.
1202    refresh: Vec<f64>,
1203}
1204
1205/// A fresh engine for one shooting run, with the lines' periodic past
1206/// (if any) at the run's period.
1207fn period_engine(
1208    circuit: &Circuit,
1209    opts: &Options,
1210    past: Option<&Past>,
1211    period: f64,
1212) -> Result<Engine, Error> {
1213    let mut e = Engine::new(circuit.build(opts)?);
1214    e.set_periodic_past(past.map(|p| p.at(period)));
1215    Ok(e)
1216}
1217
1218impl Shooter<'_> {
1219    fn engine(&self) -> Result<Engine, Error> {
1220        period_engine(self.circuit, self.opts, self.past.as_ref(), self.run.period)
1221    }
1222
1223    /// The lines' note: the history mismatch per frozen-grid
1224    /// iteration and the period mismatch of the grid refresh.
1225    fn lines_note(&self) -> Option<String> {
1226        self.past.as_ref()?;
1227        let fmt = |v: &[f64]| -> String {
1228            if v.is_empty() {
1229                "-".to_string()
1230            } else {
1231                v.iter()
1232                    .map(|r| format!("{r:.3e}"))
1233                    .collect::<Vec<_>>()
1234                    .join(", ")
1235            }
1236        };
1237        Some(format!(
1238            "pss: transmission lines read the orbit as their history (x(t − τ) = \
1239             x(t − τ + kT)): Picard on it, Newton on x0, on a frozen grid (the first \
1240             guess is the stabilization's last period); history mismatch per replay over {} \
1241             line port unknown(s): {}; grid refreshed once from the converged orbit, the \
1242             refresh run's period mismatch (the cross-grid discretization of the lines, not \
1243             iterated on): {}",
1244            self.ports.len(),
1245            fmt(&self.history),
1246            fmt(&self.refresh)
1247        ))
1248    }
1249
1250    /// The adaptive transient over the period from `x`.
1251    fn adaptive(&mut self, x: &[f64]) -> Result<PeriodOut, Error> {
1252        self.adaptive += 1;
1253        let mut log = Vec::new();
1254        let rows = self
1255            .engine()?
1256            .tran_period(self.opts, &self.run, x, &mut log)?;
1257        Ok(self.measure(rows, log, x))
1258    }
1259
1260    /// What the shooting reads off a period run from `x`: the period
1261    /// mismatch and the per-unknown tolerances.
1262    fn measure(&self, rows: Vec<(f64, Vec<f64>)>, log: Vec<StepRec>, x: &[f64]) -> PeriodOut {
1263        let n = x.len();
1264        let last = &rows.last().expect("the period has its end point").1;
1265        let mut peak = vec![0.0f64; n];
1266        for (_, sol) in &rows {
1267            for i in 1..n {
1268                peak[i] = peak[i].max(sol[i].abs());
1269            }
1270        }
1271        let mut tol = vec![1.0; n];
1272        let mut resid = vec![0.0; n];
1273        let mut rnorm = 0.0f64;
1274        for i in 1..n {
1275            let floor = match self.kinds[i] {
1276                VarKind::Branch => self.opts.abstol,
1277                _ => self.opts.vntol,
1278            };
1279            tol[i] = self.opts.reltol * peak[i] + floor;
1280            resid[i] = last[i] - x[i];
1281            rnorm = rnorm.max(resid[i].abs() / tol[i]);
1282        }
1283        PeriodOut {
1284            rows,
1285            log,
1286            resid,
1287            tol,
1288            rnorm,
1289            hnorm: 0.0,
1290            period: self.run.period,
1291        }
1292    }
1293
1294    /// Lines (issue #20): one replay of the interval from `x` on the
1295    /// frozen `grid` (an adaptive run's step log, stretched to the
1296    /// current period), the lines reading the orbit `cur` as their
1297    /// periodic past — the Picard half. The replay's rows sit at the
1298    /// times of the past's samples, so the mismatch between the orbit
1299    /// and the history it read is measured exactly. That past stays
1300    /// the shooter's for the Newton half.
1301    fn replay_on(&mut self, x: &[f64], grid: &Grid, cur: &PeriodOut) -> Result<PeriodOut, Error> {
1302        let (t0, max_step) = (self.run.t0, self.run.max_step);
1303        let past = Past::from_rows(&cur.rows, Some(&cur.log), t0, cur.period, max_step)
1304            .expect("a period run starts at t0");
1305        let (run, log) = grid.at(self.run.period);
1306        self.replays += 1;
1307        let rows = period_engine(self.circuit, self.opts, Some(&past), run.period)?
1308            .tran_replay_rows(self.opts, &run, x, &log)?;
1309        let mut r = self.measure(rows, log, x);
1310        r.hnorm = past.mismatch(&r.rows, x, t0, run.period, &self.ports, &r.tol);
1311        self.history.push(r.hnorm);
1312        self.past = Some(past);
1313        Ok(r)
1314    }
1315
1316    /// The driven shooting with transmission lines (issue #20). The
1317    /// lines' state is their history, which in the periodic regime is
1318    /// the orbit itself, `x(t − τ) = x(t − τ + kT)`; the shooting
1319    /// unknown stays `x₀` and the history is a fixed point solved
1320    /// alongside. An adaptive run (its past: the stabilization's last
1321    /// period) fixes the grid; on that frozen grid each iteration
1322    /// replays the interval with the previous orbit as the past
1323    /// (Picard) and, while the period mismatch exceeds the tolerance,
1324    /// takes a Newton step on `x₀` with that past frozen (the excited
1325    /// shooting's GMRES). Converged on the grid when both the period
1326    /// mismatch and the history mismatch are within tolerance. The
1327    /// grid is then refreshed once — an adaptive run from the result
1328    /// picks one for the converged orbit — and the iteration converges
1329    /// again on it; that orbit is the result.
1330    ///
1331    /// The frozen grid is what lets the history converge at all.
1332    /// Across grids the lines are not reproducible to the SPICE
1333    /// tolerance: the slope rule plants echo breakpoints on the *sign*
1334    /// of a slope (so two runs place a delayed corner up to a step
1335    /// apart — tens of tolerances at the corner), and the LTRA
1336    /// convolution's error is set by its grid, not by RELTOL (it has no
1337    /// LTE control). The refresh run's period mismatch is that
1338    /// cross-grid discretization, and is reported, not iterated on.
1339    fn lines_excited(
1340        &mut self,
1341        x: &mut [f64],
1342        p: &PssParams,
1343        residuals: &mut Vec<f64>,
1344    ) -> Result<PeriodOut, Error> {
1345        let fail = |residuals: &[f64]| Error::PssConvergence {
1346            iterations: residuals.len(),
1347            residuals: residuals.to_vec(),
1348            period: None,
1349        };
1350        let mut seed = self.adaptive(x)?;
1351        residuals.push(seed.rnorm);
1352        for pass in 0..GRID_PASSES {
1353            let grid = Grid::of(self.run, &seed.log);
1354            let mut cur = seed;
1355            let done = loop {
1356                if residuals.len() >= p.max_iter {
1357                    return Err(fail(residuals));
1358                }
1359                let r = self.replay_on(x, &grid, &cur)?;
1360                residuals.push(r.total());
1361                if r.total() <= p.steady_coeff {
1362                    break r;
1363                }
1364                if r.rnorm > p.steady_coeff {
1365                    let step = self.newton_step(x, &r)?;
1366                    for i in 1..x.len() {
1367                        x[i] += step[i];
1368                    }
1369                }
1370                cur = r;
1371            };
1372            if pass + 1 == GRID_PASSES {
1373                return Ok(done);
1374            }
1375            self.past = Some(self.past_of(&done));
1376            seed = self.adaptive(x)?;
1377            residuals.push(seed.rnorm);
1378            self.refresh.push(seed.rnorm);
1379        }
1380        unreachable!("the last pass returns")
1381    }
1382
1383    /// The autonomous shooting with transmission lines: the scheme of
1384    /// [`Shooter::lines_excited`] with the Newton step of
1385    /// [`Shooter::newton_step_osc`] (`x₀` and `T`; the frozen grid and
1386    /// the past stretch with `T`, both being functions of phase), at
1387    /// least one Newton step (D-0035). Returns the orbit and the last
1388    /// period correction.
1389    #[allow(clippy::too_many_arguments)]
1390    fn lines_autonomous(
1391        &mut self,
1392        x: &mut [f64],
1393        period: &mut f64,
1394        osc: usize,
1395        p: &PssParams,
1396        run_for: &dyn Fn(f64) -> PeriodRun,
1397        residuals: &mut Vec<f64>,
1398        periods: &mut Vec<f64>,
1399    ) -> Result<(PeriodOut, f64), Error> {
1400        let fail = |residuals: &[f64], period: f64| Error::PssConvergence {
1401            iterations: residuals.len(),
1402            residuals: residuals.to_vec(),
1403            period: Some(period),
1404        };
1405        self.run = run_for(*period);
1406        let mut seed = self.adaptive(x)?;
1407        residuals.push(seed.rnorm);
1408        periods.push(*period);
1409        let mut dt = 0.0;
1410        for pass in 0..GRID_PASSES {
1411            let grid = Grid::of(self.run, &seed.log);
1412            let mut cur = seed;
1413            let mut stepped = false;
1414            let done = loop {
1415                if residuals.len() >= p.max_iter {
1416                    return Err(fail(residuals, *period));
1417                }
1418                self.run = run_for(*period);
1419                let r = self.replay_on(x, &grid, &cur)?;
1420                residuals.push(r.total());
1421                periods.push(*period);
1422                if r.total() <= p.steady_coeff && stepped {
1423                    break r;
1424                }
1425                if r.rnorm > p.steady_coeff || !stepped {
1426                    let (mut sx, mut st) = self.newton_step_osc(x, *period, osc, &r)?;
1427                    if st.abs() > MAX_DT * *period {
1428                        let k = MAX_DT * *period / st.abs();
1429                        sx.iter_mut().for_each(|v| *v *= k);
1430                        st *= k;
1431                    }
1432                    for i in 1..x.len() {
1433                        x[i] += sx[i];
1434                    }
1435                    *period += st;
1436                    dt = st;
1437                    stepped = true;
1438                }
1439                cur = r;
1440            };
1441            if pass + 1 == GRID_PASSES {
1442                return Ok((done, dt));
1443            }
1444            self.run = run_for(*period);
1445            self.past = Some(self.past_of(&done));
1446            seed = self.adaptive(x)?;
1447            residuals.push(seed.rnorm);
1448            periods.push(*period);
1449            self.refresh.push(seed.rnorm);
1450        }
1451        unreachable!("the last pass returns")
1452    }
1453
1454    /// The periodic past a converged orbit leaves for the next run.
1455    fn past_of(&self, done: &PeriodOut) -> Past {
1456        Past::from_rows(
1457            &done.rows,
1458            Some(&done.log),
1459            self.run.t0,
1460            done.period,
1461            self.run.max_step,
1462        )
1463        .expect("a period run starts at t0")
1464    }
1465
1466    /// The Newton step `δ` of `(I − Φ′) δ = Φ(x) − x`, by GMRES on the
1467    /// scaled unknowns `x_i / tol_i` with `Φ′ v` from frozen-grid
1468    /// replays. The replays first run at tightened Newton tolerances;
1469    /// if one of them fails, the step is redone at the deck's.
1470    fn newton_step(&mut self, x: &[f64], at: &PeriodOut) -> Result<Vec<f64>, Error> {
1471        match self.newton_step_with(x, at, true) {
1472            Ok(s) => Ok(s),
1473            Err(Error::IterLim) => self.newton_step_with(x, at, false),
1474            Err(e) => Err(e),
1475        }
1476    }
1477
1478    fn newton_step_with(
1479        &mut self,
1480        x: &[f64],
1481        at: &PeriodOut,
1482        tight: bool,
1483    ) -> Result<Vec<f64>, Error> {
1484        let opts = if tight { &self.tight } else { self.opts };
1485        let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1486        let past = self.past.as_ref();
1487        let n = x.len();
1488        let d = &at.tol;
1489        let replays = std::cell::Cell::new(0usize);
1490        let replay = |x0: &[f64]| -> Result<Vec<f64>, Error> {
1491            replays.set(replays.get() + 1);
1492            period_engine(circuit, build_opts, past, run.period)?
1493                .tran_replay(opts, &run, x0, &at.log)
1494        };
1495        let base = replay(x)?;
1496        // Scaled residual, unknowns 1..n.
1497        let b: Vec<f64> = (1..n).map(|i| at.resid[i] / d[i]).collect();
1498        let bnorm = norm(&b);
1499        let lin_tol = f64::min(1e-2, 1e-4 * bnorm);
1500        let mut apply = |v: &[f64]| -> Result<Vec<f64>, Error> {
1501            let mut xp = x.to_vec();
1502            for i in 1..n {
1503                xp[i] += FD_EPS * d[i] * v[i - 1];
1504            }
1505            let y = replay(&xp)?;
1506            Ok((1..n)
1507                .map(|i| v[i - 1] - (y[i] - base[i]) / (FD_EPS * d[i]))
1508                .collect())
1509        };
1510        let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1511        self.replays += replays.get();
1512        let (ds, dim) = solved?;
1513        self.krylov.push(dim);
1514        let mut step = vec![0.0; n];
1515        for i in 1..n {
1516            step[i] = ds[i - 1] * d[i];
1517        }
1518        Ok(step)
1519    }
1520
1521    /// The Newton step `(δx, δT)` of an autonomous oscillator: the
1522    /// bordered system `(I − Φ′) δx − (∂Φ/∂T) δT = Φ_T(x) − x` with
1523    /// `δx[osc] = 0` (the phase condition fixes OSCNODE), square
1524    /// again because `T` takes the place of `x[osc]` among the
1525    /// unknowns. GMRES on the scaled unknowns (`x_i / tol_i`,
1526    /// `T / (RELTOL·T)`); one replay per product perturbs `x` and `T`
1527    /// together, `T` by stretching the frozen grid uniformly — the
1528    /// replay is then smooth in `T` as it is in `x` (every step, and
1529    /// every coefficient of the integration formulas, scales with it).
1530    fn newton_step_osc(
1531        &mut self,
1532        x: &[f64],
1533        period: f64,
1534        osc: usize,
1535        at: &PeriodOut,
1536    ) -> Result<(Vec<f64>, f64), Error> {
1537        match self.newton_step_osc_with(x, period, osc, at, true) {
1538            Ok(s) => Ok(s),
1539            Err(Error::IterLim) => self.newton_step_osc_with(x, period, osc, at, false),
1540            Err(e) => Err(e),
1541        }
1542    }
1543
1544    fn newton_step_osc_with(
1545        &mut self,
1546        x: &[f64],
1547        period: f64,
1548        osc: usize,
1549        at: &PeriodOut,
1550        tight: bool,
1551    ) -> Result<(Vec<f64>, f64), Error> {
1552        let opts = if tight { &self.tight } else { self.opts };
1553        let (circuit, build_opts, run) = (self.circuit, self.opts, self.run);
1554        let n = x.len();
1555        let d = &at.tol;
1556        let s_t = self.opts.reltol * period;
1557        // Unknown i sits in slot i − 1 and T in OSCNODE's slot, so
1558        // the operator stays identity plus low rank (the identity's
1559        // osc entry is replaced by the −∂Φ/∂T column), which is what
1560        // makes GMRES converge in a few products.
1561        let slot = |i: usize| i - 1;
1562        let t_slot = osc - 1;
1563        let replays = std::cell::Cell::new(0usize);
1564        let past = self.past.as_ref();
1565        let replay = |x0: &[f64], t: f64| -> Result<Vec<f64>, Error> {
1566            replays.set(replays.get() + 1);
1567            // Lines: the past stretches with the period (it is the
1568            // orbit as a function of phase).
1569            let mut engine = period_engine(circuit, build_opts, past, t)?;
1570            if t == period {
1571                return engine.tran_replay(opts, &run, x0, &at.log);
1572            }
1573            let k = t / period;
1574            let stretched = PeriodRun {
1575                t0: run.t0,
1576                period: t,
1577                tstep: run.tstep * k,
1578                max_step: run.max_step * k,
1579            };
1580            let mut log: Vec<StepRec> = at
1581                .log
1582                .iter()
1583                .map(|r| StepRec {
1584                    time: run.t0 + (r.time - run.t0) * k,
1585                    ..*r
1586                })
1587                .collect();
1588            if let Some(end) = log.last_mut() {
1589                end.time = run.t0 + t;
1590            }
1591            engine.tran_replay(opts, &stretched, x0, &log)
1592        };
1593        let base = replay(x, period)?;
1594        let b: Vec<f64> = (1..n).map(|i| at.resid[i] / d[i]).collect();
1595        let bnorm = norm(&b);
1596        let lin_tol = f64::min(1e-2, 1e-4 * bnorm);
1597        let mut apply = |u: &[f64]| -> Result<Vec<f64>, Error> {
1598            let mut xp = x.to_vec();
1599            for i in 1..n {
1600                if i != osc {
1601                    xp[i] += FD_EPS * d[i] * u[slot(i)];
1602                }
1603            }
1604            let y = replay(&xp, period + FD_EPS * s_t * u[t_slot])?;
1605            Ok((1..n)
1606                .map(|i| {
1607                    let ui = if i == osc { 0.0 } else { u[slot(i)] };
1608                    ui - (y[i] - base[i]) / (FD_EPS * d[i])
1609                })
1610                .collect())
1611        };
1612        let solved = gmres(&b, n - 1, lin_tol, &mut apply);
1613        self.replays += replays.get();
1614        let (us, dim) = solved?;
1615        self.krylov.push(dim);
1616        let mut step = vec![0.0; n];
1617        for i in 1..n {
1618            if i != osc {
1619                step[i] = us[slot(i)] * d[i];
1620            }
1621        }
1622        Ok((step, us[t_slot] * s_t))
1623    }
1624
1625    fn summary(&self, residuals: &[f64]) -> String {
1626        let hist: Vec<String> = residuals.iter().map(|r| format!("{r:.3e}")).collect();
1627        format!(
1628            "pss: {} shooting iteration(s), residual/tolerance per iteration: {}; \
1629             {} adaptive and {} replayed period transients (GMRES dimensions {:?})",
1630            residuals.len(),
1631            hist.join(", "),
1632            self.adaptive,
1633            self.replays,
1634            self.krylov
1635        )
1636    }
1637}
1638
1639/// A matrix-free operator `v ↦ A v` (one product may run transients).
1640type LinearMap<'a> = dyn FnMut(&[f64]) -> Result<Vec<f64>, Error> + 'a;
1641
1642fn norm(v: &[f64]) -> f64 {
1643    v.iter().map(|x| x * x).sum::<f64>().sqrt()
1644}
1645
1646/// Restart-free GMRES (Saad–Schultz, modified Gram–Schmidt with one
1647/// reorthogonalization, Givens rotations) for `A y = b` from `y = 0`,
1648/// at most `m` iterations, stopping at residual `≤ tol`. Returns the
1649/// solution and the Krylov dimension used.
1650fn gmres(b: &[f64], m: usize, tol: f64, apply: &mut LinearMap) -> Result<(Vec<f64>, usize), Error> {
1651    let n = b.len();
1652    let beta = norm(b);
1653    if beta == 0.0 || m == 0 {
1654        return Ok((vec![0.0; n], 0));
1655    }
1656    let mut v: Vec<Vec<f64>> = vec![b.iter().map(|x| x / beta).collect()];
1657    let mut h: Vec<Vec<f64>> = Vec::new(); // columns, each of length j + 2
1658    let mut cs: Vec<f64> = Vec::new();
1659    let mut sn: Vec<f64> = Vec::new();
1660    let mut g = vec![beta];
1661    let mut k = 0;
1662    while k < m {
1663        let mut w = apply(&v[k])?;
1664        let mut col = vec![0.0; k + 2];
1665        for _pass in 0..2 {
1666            for (i, vi) in v.iter().enumerate() {
1667                let c: f64 = w.iter().zip(vi).map(|(a, b)| a * b).sum();
1668                col[i] += c;
1669                for (wj, vj) in w.iter_mut().zip(vi) {
1670                    *wj -= c * vj;
1671                }
1672            }
1673        }
1674        let hn = norm(&w);
1675        col[k + 1] = hn;
1676        for i in 0..k {
1677            let t = cs[i] * col[i] + sn[i] * col[i + 1];
1678            col[i + 1] = -sn[i] * col[i] + cs[i] * col[i + 1];
1679            col[i] = t;
1680        }
1681        let r = col[k].hypot(col[k + 1]);
1682        let (c, s) = if r == 0.0 {
1683            (1.0, 0.0)
1684        } else {
1685            (col[k] / r, col[k + 1] / r)
1686        };
1687        col[k] = r;
1688        col[k + 1] = 0.0;
1689        cs.push(c);
1690        sn.push(s);
1691        g.push(-s * g[k]);
1692        g[k] *= c;
1693        h.push(col);
1694        k += 1;
1695        let breakdown = hn <= 1e-14 * beta;
1696        if g[k].abs() <= tol || breakdown {
1697            break;
1698        }
1699        v.push(w.iter().map(|x| x / hn).collect());
1700    }
1701    // Back substitution on the k×k triangle.
1702    let mut y = vec![0.0; k];
1703    for i in (0..k).rev() {
1704        let mut s = g[i];
1705        for j in i + 1..k {
1706            s -= h[j][i] * y[j];
1707        }
1708        y[i] = if h[i][i] != 0.0 { s / h[i][i] } else { 0.0 };
1709    }
1710    let mut x = vec![0.0; n];
1711    for (j, yj) in y.iter().enumerate() {
1712        for (xi, vi) in x.iter_mut().zip(&v[j]) {
1713            *xi += yj * vi;
1714        }
1715    }
1716    Ok((x, k))
1717}
1718
1719/// The time-domain and frequency-domain plots of the converged period.
1720fn outputs(
1721    per: &PeriodOut,
1722    names: &[String],
1723    kinds: &[VarKind],
1724    p: &PssParams,
1725    t0: f64,
1726    period: f64,
1727) -> (Solution, Solution) {
1728    let n_pts = p.points;
1729    let times: Vec<f64> = (0..=n_pts)
1730        .map(|k| t0 + period * k as f64 / n_pts as f64)
1731        .collect();
1732    // The rows at breakpoints (and both ends): a stencil never
1733    // straddles a source corner.
1734    let rows = &per.rows;
1735    let mut breaks = vec![0usize];
1736    breaks.extend((1..rows.len()).filter(|&j| per.log[j - 1].brk));
1737    if *breaks.last().unwrap_or(&0) != rows.len() - 1 {
1738        breaks.push(rows.len() - 1);
1739    }
1740
1741    let mut variables = vec![("time".to_string(), VarType::Time)];
1742    let mut sel: Vec<usize> = Vec::new();
1743    for i in 1..names.len() {
1744        match kinds[i] {
1745            VarKind::Node if saved_node(&names[i]) => {
1746                variables.push((format!("v({})", names[i]), VarType::Voltage));
1747                sel.push(i);
1748            }
1749            VarKind::Branch => {
1750                variables.push((format!("i({})", names[i]), VarType::Current));
1751                sel.push(i);
1752            }
1753            _ => {}
1754        }
1755    }
1756    let mut cols: Vec<Vec<f64>> = vec![times.clone()];
1757    cols.extend(sel.iter().map(|_| Vec::with_capacity(times.len())));
1758    for (k, &t) in times.iter().enumerate() {
1759        let vals: Vec<f64> = if k == n_pts {
1760            rows[rows.len() - 1].1.clone()
1761        } else if k == 0 {
1762            rows[0].1.clone()
1763        } else {
1764            interp(rows, &breaks, t)
1765        };
1766        for (c, &i) in sel.iter().enumerate() {
1767            cols[c + 1].push(vals[i]);
1768        }
1769    }
1770
1771    // DFT of the first `points` samples (the last one closes the period).
1772    let h = p.harmonics;
1773    let nn = n_pts as f64;
1774    let mut fvars = vec![("frequency".to_string(), VarType::Frequency)];
1775    fvars.extend(variables[1..].iter().cloned());
1776    let mut fcols: Vec<Vec<Complex64>> = vec![
1777        (0..h)
1778            .map(|k| Complex64::new(k as f64 / period, 0.0))
1779            .collect(),
1780    ];
1781    for col in &cols[1..] {
1782        let samples = &col[..n_pts];
1783        let mut out = Vec::with_capacity(h);
1784        for k in 0..h {
1785            let mut acc = Complex64::new(0.0, 0.0);
1786            for (m, &s) in samples.iter().enumerate() {
1787                // Reduce k·m mod N before the angle: exact arguments.
1788                let km = (k * m) % n_pts;
1789                let ang = -2.0 * std::f64::consts::PI * km as f64 / nn;
1790                acc += s * Complex64::new(ang.cos(), ang.sin());
1791            }
1792            out.push(if k == 0 { acc / nn } else { acc * (2.0 / nn) });
1793        }
1794        fcols.push(out);
1795    }
1796    (
1797        Solution {
1798            plotname: PSS_TIME_PLOT.to_string(),
1799            variables,
1800            data: Data::Real(cols),
1801            scale: Some(0),
1802        },
1803        Solution {
1804            plotname: PSS_FREQ_PLOT.to_string(),
1805            variables: fvars,
1806            data: Data::Complex(fcols),
1807            scale: Some(0),
1808        },
1809    )
1810}
1811
1812/// The solution at `t` by cubic Lagrange interpolation over the four
1813/// accepted points around it, kept inside the breakpoint segment
1814/// (lower order when the segment has fewer points; `breaks` are the
1815/// sorted breakpoint rows, both ends included).
1816fn interp(rows: &[(f64, Vec<f64>)], breaks: &[usize], t: f64) -> Vec<f64> {
1817    // Rows j and j+1 bracket t.
1818    let j = rows.partition_point(|r| r.0 <= t).clamp(1, rows.len() - 1) - 1;
1819    let b = breaks.partition_point(|&b| b <= j);
1820    let lo = breaks[b - 1];
1821    let hi = breaks.get(b).copied().unwrap_or(rows.len() - 1).max(j + 1);
1822    let want = 4usize.min(hi - lo + 1);
1823    let mut a = j.saturating_sub(1).max(lo);
1824    if a + want - 1 > hi {
1825        a = hi + 1 - want;
1826    }
1827    let n = rows[0].1.len();
1828    let mut out = vec![0.0; n];
1829    for p in a..a + want {
1830        let mut w = 1.0;
1831        for q in a..a + want {
1832            if q != p {
1833                w *= (t - rows[q].0) / (rows[p].0 - rows[q].0);
1834            }
1835        }
1836        for (o, v) in out.iter_mut().zip(&rows[p].1) {
1837            *o += w * v;
1838        }
1839    }
1840    out
1841}
1842
1843#[cfg(test)]
1844mod tests {
1845    use super::*;
1846
1847    #[test]
1848    fn harvest_takes_the_common_fundamental() {
1849        let s = |f: &[f64]| -> Vec<(String, f64)> { f.iter().map(|&f| ("v".into(), f)).collect() };
1850        assert!((harvest(&s(&[1e3])).unwrap() - 1e3).abs() < 1e-9);
1851        assert!((harvest(&s(&[1e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1852        assert!((harvest(&s(&[2e3, 3e3])).unwrap() - 1e3).abs() < 1e-6);
1853        assert!((harvest(&s(&[50e3, 1.0 / 20e-6])).unwrap() - 50e3).abs() < 1e-3);
1854        assert!(harvest(&s(&[1.0, std::f64::consts::PI])).is_err());
1855        assert!(harvest(&[]).is_err());
1856    }
1857
1858    #[test]
1859    fn gmres_solves_a_small_system() {
1860        // A = I - u wᵀ (a rank-one monodromy): converges in 2 steps.
1861        let u = [0.5, -0.2, 0.1];
1862        let w = [0.3, 0.4, -0.6];
1863        let mut apply = |v: &[f64]| -> Result<Vec<f64>, Error> {
1864            let d: f64 = w.iter().zip(v).map(|(a, b)| a * b).sum();
1865            Ok(v.iter().zip(&u).map(|(vi, ui)| vi - ui * d).collect())
1866        };
1867        let b = [1.0, 2.0, 3.0];
1868        let (y, k) = gmres(&b, 3, 1e-13, &mut apply).unwrap();
1869        assert!(k <= 2);
1870        let ay = apply(&y).unwrap();
1871        for (a, b) in ay.iter().zip(&b) {
1872            assert!((a - b).abs() < 1e-12);
1873        }
1874    }
1875}