Skip to main content

dune_core/
pz.rs

1//! `.pz` — pole-zero analysis (`ckt/pzan.c`, `ckt/cktpzset.c`,
2//! `ckt/cktpzld.c`, `ckt/cktpzstr.c`, `ni/nipzmeth.c`). The roots of
3//! the transfer function's denominator (poles) and numerator (zeros)
4//! are hunted on the complex plane as zeros of the MNA determinant
5//! after the column surgery of `CKTpzSetup`: a "zeroin" variant over
6//! the real axis with Muller's method for complex pairs, deflation
7//! of found roots, and the block-normalized (mantissa, base-2
8//! exponent) arithmetic the 3f5 uses throughout.
9
10use dune_raw::{Data, VarType};
11use num_complex::Complex64;
12
13use crate::circuit::Circuit;
14use crate::engine::{Engine, PzSetup};
15use crate::error::Error;
16use crate::matrix::{AnKind, InitMode, Var};
17use crate::options::Options;
18use crate::solution::Solution;
19
20/// `.pz` input coupling (`CUR`/`VOL`).
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22pub enum PzInput {
23    Current,
24    Voltage,
25}
26
27/// What to find (`POL`/`ZER`/`PZ`).
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum PzWhich {
30    Poles,
31    Zeros,
32    Both,
33}
34
35/// Result of a `.pz` analysis.
36#[derive(Clone, Debug)]
37pub struct Pz {
38    pub solution: Solution,
39    pub notes: Vec<String>,
40}
41
42impl Circuit {
43    /// Run `.pz NODE1 NODE2 NODE3 NODE4 CUR|VOL POL|ZER|PZ`.
44    pub fn pz(
45        &self,
46        opts: &Options,
47        nodes: [&str; 4],
48        input: PzInput,
49        which: PzWhich,
50    ) -> Result<Pz, Error> {
51        let built = self.build(opts)?;
52        let notes = built.notes.clone();
53        let mut engine = Engine::new(built);
54
55        let var_of = |name: &str| -> Result<Var, Error> {
56            let name = name.to_ascii_lowercase();
57            if name == "0" {
58                return Ok(0);
59            }
60            engine
61                .vars
62                .names
63                .iter()
64                .position(|n| *n == name)
65                .ok_or_else(|| Error::Build(format!(".pz names unknown node `{name}`")))
66        };
67        let in_pos = var_of(nodes[0])?;
68        let in_neg = var_of(nodes[1])?;
69        let out_pos = var_of(nodes[2])?;
70        let out_neg = var_of(nodes[3])?;
71
72        // `PZinit` error checks.
73        if in_pos == in_neg {
74            return Err(Error::Build(".pz: input is shorted".into()));
75        }
76        if out_pos == out_neg {
77            return Err(Error::Build(".pz: output is shorted".into()));
78        }
79        if input == PzInput::Voltage {
80            if in_pos == out_pos && in_neg == out_neg {
81                return Err(Error::Build(".pz: transfer function is unity".into()));
82            }
83            if in_pos == out_neg && in_neg == out_pos {
84                return Err(Error::Build(".pz: transfer function is -1".into()));
85            }
86        }
87
88        // Operating point + small-signal parameter pass (`PZan`).
89        engine.set_kind(AnKind::DcOp, false);
90        engine.op(opts)?;
91        engine.load_all_public(InitMode::Smsig, opts);
92
93        let vol = input == PzInput::Voltage;
94        let mut poles: Vec<(Complex64, i32)> = Vec::new();
95        let mut zeros: Vec<(Complex64, i32)> = Vec::new();
96        if which != PzWhich::Zeros {
97            let mut setup = engine.pz_setup(in_pos, in_neg, out_pos, out_neg, vol, false);
98            poles = find_zeros(&mut engine, &mut setup)?;
99        }
100        if which != PzWhich::Poles {
101            let mut setup = engine.pz_setup(in_pos, in_neg, out_pos, out_neg, vol, true);
102            zeros = find_zeros(&mut engine, &mut setup)?;
103        }
104
105        // `PZpost`: expand multiplicity and conjugate pairs, poles
106        // first, with the ngspice rawfile names.
107        let mut variables = Vec::new();
108        let mut columns: Vec<Vec<Complex64>> = Vec::new();
109        let mut emit = |list: &[(Complex64, i32)], label: &str| {
110            let mut k = 0usize;
111            for &(s, mult) in list {
112                for _ in 0..mult {
113                    k += 1;
114                    variables.push((format!("v({label}({k}))"), VarType::Voltage));
115                    columns.push(vec![s]);
116                    if s.im != 0.0 {
117                        k += 1;
118                        variables.push((format!("v({label}({k}))"), VarType::Voltage));
119                        columns.push(vec![s.conj()]);
120                    }
121                }
122            }
123        };
124        emit(&poles, "pole");
125        emit(&zeros, "zero");
126        if variables.is_empty() {
127            return Err(Error::Build(".pz found no poles or zeros".into()));
128        }
129
130        Ok(Pz {
131            solution: Solution {
132                plotname: "Pole-Zero Analysis".to_string(),
133                variables,
134                data: Data::Complex(columns),
135                scale: None,
136            },
137            notes,
138        })
139    }
140}
141
142// ---------------------------------------------------------------
143// The root finder (`cktpzstr.c` / `nipzmeth.c`), ported with its
144// heuristics intact. The 3f5 keeps trials in a doubly linked list
145// ordered by `s`; here the list nodes live in a Vec and links are
146// indices.
147// ---------------------------------------------------------------
148
149const NITER_LIM: usize = 200;
150
151// Strategies (same numbering as the C so `< GUESS` keeps meaning).
152const SHIFT_LEFT: i32 = 2;
153const SHIFT_RIGHT: i32 = 3;
154const SKIP_LEFT: i32 = 4;
155const SKIP_RIGHT: i32 = 5;
156const INIT: i32 = 6;
157const GUESS: i32 = 7;
158const SPLIT_LEFT: i32 = 8;
159const SPLIT_RIGHT: i32 = 9;
160const MULLER: i32 = 10;
161const SYM: i32 = 11;
162const SYM2: i32 = 12;
163const COMPLEX_INIT: i32 = 13;
164const COMPLEX_GUESS: i32 = 14;
165
166// Moves.
167const NEAR_LEFT: i32 = 4;
168const MID_LEFT: i32 = 5;
169const FAR_LEFT: i32 = 6;
170const NEAR_RIGHT: i32 = 7;
171const FAR_RIGHT: i32 = 8;
172const MID_RIGHT: i32 = 9;
173
174const ISAROOT: u8 = 2;
175#[allow(dead_code)]
176const ISAREPEAT: u8 = 4;
177const ISAMINIMA: u8 = 16;
178
179fn sgn(x: f64) -> i32 {
180    if x < 0.0 {
181        -1
182    } else if x == 0.0 {
183        0
184    } else {
185        1
186    }
187}
188
189/// `zaddeq`: add two block-normalized reals.
190fn zaddeq(x: f64, xmag: i32, y: f64, ymag: i32) -> (f64, i32) {
191    let (mut x, mut y) = (x, y);
192    let amag;
193    if xmag > ymag {
194        amag = xmag;
195        if xmag > 50 + ymag {
196            y = 0.0;
197        } else {
198            let mut d = xmag - ymag;
199            while d > 0 {
200                y /= 2.0;
201                d -= 1;
202            }
203        }
204    } else {
205        amag = ymag;
206        if ymag > 50 + xmag {
207            x = 0.0;
208        } else {
209            let mut d = ymag - xmag;
210            while d > 0 {
211                x /= 2.0;
212                d -= 1;
213            }
214        }
215    }
216    let mut a = x + y;
217    let mut m = amag;
218    if a == 0.0 {
219        m = 0;
220    } else {
221        while a.abs() > 1.0 {
222            a /= 2.0;
223            m += 1;
224        }
225        while a.abs() < 0.5 {
226            a *= 2.0;
227            m -= 1;
228        }
229    }
230    (a, m)
231}
232
233/// `R_NORM`.
234fn r_norm(a: &mut f64, b: &mut i32) {
235    if *a == 0.0 {
236        *b = 0;
237    } else {
238        while a.abs() > 1.0 {
239            *b += 1;
240            *a /= 2.0;
241        }
242        while a.abs() < 0.5 {
243            *b -= 1;
244            *a *= 2.0;
245        }
246    }
247}
248
249/// `C_NORM`.
250fn c_norm(a: &mut Complex64, b: &mut i32) {
251    if a.re == 0.0 && a.im == 0.0 {
252        *b = 0;
253    } else {
254        while a.re.abs() > 1.0 || a.im.abs() > 1.0 {
255            *b += 1;
256            a.re /= 2.0;
257            a.im /= 2.0;
258        }
259        while a.re.abs() <= 0.5 && a.im.abs() <= 0.5 {
260            *b -= 1;
261            a.re *= 2.0;
262            a.im *= 2.0;
263        }
264    }
265}
266
267/// `C_SQRT`.
268fn c_sqrt(a: &mut Complex64) {
269    if a.im == 0.0 {
270        if a.re < 0.0 {
271            a.im = (-a.re).sqrt();
272            a.re = 0.0;
273        } else {
274            a.re = a.re.sqrt();
275            a.im = 0.0;
276        }
277    } else {
278        let mag = (a.re * a.re + a.im * a.im).sqrt();
279        let half = (mag - a.re) / 2.0;
280        if half <= 0.0 {
281            a.re = mag.sqrt();
282            a.im /= 2.0 * a.re;
283        } else {
284            let half = half.sqrt();
285            a.re = a.im / (2.0 * half);
286            a.im = half;
287        }
288    }
289}
290
291#[derive(Clone)]
292struct Trial {
293    s: Complex64,
294    f_raw: Complex64,
295    mag_raw: i32,
296    f_def: Complex64,
297    mag_def: i32,
298    multiplicity: i32,
299    count: i32,
300    #[allow(dead_code)]
301    seq: i32,
302    flags: u8,
303    prev: Option<usize>,
304    next: Option<usize>,
305    dead: bool,
306}
307
308struct Finder<'a> {
309    eng: &'a mut Engine,
310    setup: &'a mut PzSetup,
311    nodes: Vec<Trial>,
312    head: Option<usize>,
313    zero_trial: Option<usize>,
314    nzeros: usize,
315    nflat: i32,
316    max_zeros: usize,
317    seq: i32,
318    guess_param: f64,
319    high_guess: f64,
320    low_guess: f64,
321    last_move: i32,
322    consec_moves: i32,
323    niter: usize,
324    ntrials: i32,
325    trapped: i32,
326    aberr: i32,
327    nipzk: f64,
328    nipzk_mag: i32,
329}
330
331/// A trial candidate before it enters the list.
332struct Cand {
333    s: Complex64,
334    seq: i32,
335}
336
337type Set = [Option<usize>; 3];
338
339/// `CKTpzFindZeros`: returns the roots found, ascending in `s`,
340/// with their multiplicities.
341fn find_zeros(eng: &mut Engine, setup: &mut PzSetup) -> Result<Vec<(Complex64, i32)>, Error> {
342    let max_zeros = eng.vars.len() - 1;
343    let mut f = Finder {
344        eng,
345        setup,
346        nodes: Vec::new(),
347        head: None,
348        zero_trial: None,
349        nzeros: 0,
350        nflat: 0,
351        max_zeros,
352        seq: 1,
353        guess_param: 0.0,
354        high_guess: -1.0,
355        low_guess: 1.0,
356        last_move: 0,
357        consec_moves: 0,
358        niter: 0,
359        ntrials: 0,
360        trapped: 0,
361        aberr: 0,
362        nipzk: 0.0,
363        nipzk_mag: 0,
364    };
365
366    let mut set: Set = [None; 3];
367    f.reset(&mut set);
368
369    loop {
370        let mut strat;
371        loop {
372            strat = f.strat(&set);
373            if !(strat < GUESS && f.trapped == 0) {
374                break;
375            }
376            if !f.step(strat, &mut set) {
377                strat = GUESS;
378                break;
379            }
380        }
381
382        f.niter += 1;
383
384        let cand = f.eval(strat, &set)?;
385        let idx = f.run_trial(cand, &set)?;
386
387        if f.nodes[idx].flags & ISAROOT != 0 {
388            f.verify(idx);
389            f.niter = 0;
390            f.reset(&mut set);
391        } else if f.nodes[idx].flags & ISAMINIMA != 0 {
392            set[0] = None;
393            set[1] = Some(idx);
394            set[2] = None;
395        } else {
396            f.update_set(&mut set, idx);
397        }
398
399        // Loop exit (the C's do/while condition).
400        let spread_ok = f.high_guess - f.low_guess < 1e35;
401        let window_ok = match (set[0], set[2]) {
402            (Some(a), Some(c)) => f.trapped != 0 || f.nodes[c].s.re - f.nodes[a].s.re < 1e22,
403            _ => true,
404        };
405        if !(spread_ok && f.nzeros < f.max_zeros && f.niter < NITER_LIM && f.aberr < 3 && window_ok)
406        {
407            break;
408        }
409    }
410
411    if f.nzeros as i32 >= f.seq - 1 {
412        return Err(Error::Build(
413            ".pz: the input signal is shorted on the way to the output".into(),
414        ));
415    }
416
417    // `clear_trials(0)`: keep the roots, ascending.
418    let mut out = Vec::new();
419    let mut cur = f.head;
420    while let Some(i) = cur {
421        let t = &f.nodes[i];
422        if !t.dead && t.flags & ISAROOT != 0 {
423            out.push((t.s, t.multiplicity));
424        }
425        cur = t.next;
426    }
427    Ok(out)
428}
429
430impl Finder<'_> {
431    fn t(&self, i: usize) -> &Trial {
432        &self.nodes[i]
433    }
434
435    /// `pzseek`.
436    fn seek(&mut self, from: Option<usize>, dir: i32) -> Option<usize> {
437        self.guess_param = dir as f64;
438        let mut t = from?;
439        if dir == 0 && self.t(t).flags & (ISAROOT | ISAMINIMA) == 0 {
440            return Some(t);
441        }
442        loop {
443            let next = if dir >= 0 {
444                self.t(t).next
445            } else {
446                self.t(t).prev
447            };
448            t = next?;
449            if self.t(t).flags & (ISAROOT | ISAMINIMA) == 0 {
450                return Some(t);
451            }
452        }
453    }
454
455    /// `CKTpzReset`.
456    fn reset(&mut self, set: &mut Set) {
457        self.trapped = 0;
458        self.consec_moves = 0;
459        set[1] = self.seek(self.zero_trial, 0);
460        if set[1].is_some() {
461            set[0] = self.seek(set[1], -1);
462            set[2] = self.seek(set[1], 1);
463        } else {
464            set[0] = None;
465            set[2] = None;
466        }
467    }
468
469    /// `CKTpzStep`.
470    fn step(&mut self, strat: i32, set: &mut Set) -> bool {
471        match strat {
472            INIT => {
473                if set[1].is_none() {
474                    set[1] = self.seek(self.zero_trial, 0);
475                } else if set[2].is_none() {
476                    set[2] = self.seek(set[1], 1);
477                } else if set[0].is_none() {
478                    set[0] = self.seek(set[1], -1);
479                }
480            }
481            SKIP_LEFT => set[0] = self.seek(set[0], -1),
482            SKIP_RIGHT => set[2] = self.seek(set[2], 1),
483            SHIFT_LEFT => {
484                set[2] = set[1];
485                set[1] = set[0];
486                set[0] = self.seek(set[0], -1);
487            }
488            SHIFT_RIGHT => {
489                set[0] = set[1];
490                set[1] = set[2];
491                set[2] = self.seek(set[2], 1);
492            }
493            _ => {}
494        }
495        set[0].is_some() && set[1].is_some() && set[2].is_some()
496    }
497
498    /// `CKTpzStrat`.
499    fn strat(&mut self, set: &Set) -> i32 {
500        let mut new_trap = 0;
501        let suggestion;
502
503        if set[1].is_some() && self.t(set[1].unwrap()).flags & ISAMINIMA != 0 {
504            suggestion = COMPLEX_INIT;
505        } else if set[0].is_some() && self.t(set[0].unwrap()).s.im != 0.0 {
506            if set[1].is_none() || set[2].is_none() {
507                suggestion = COMPLEX_GUESS;
508            } else {
509                suggestion = MULLER;
510            }
511        } else if set[0].is_none() || set[1].is_none() || set[2].is_none() {
512            suggestion = INIT;
513        } else {
514            let t0 = self.t(set[0].unwrap()).clone();
515            let t1 = self.t(set[1].unwrap()).clone();
516            let t2 = self.t(set[2].unwrap()).clone();
517            let mut sug;
518            if sgn(t0.f_def.re) != sgn(t1.f_def.re) {
519                new_trap = 1;
520                sug = SYM2;
521            } else if sgn(t1.f_def.re) != sgn(t2.f_def.re) {
522                new_trap = 2;
523                sug = SYM2;
524            } else {
525                let (a, a_mag) = zaddeq(t1.f_def.re, t1.mag_def, -t0.f_def.re, t0.mag_def);
526                let (b, b_mag) = zaddeq(t2.f_def.re, t2.mag_def, -t1.f_def.re, t1.mag_def);
527                if self.trapped == 0 {
528                    let k1 = t1.s.re - t0.s.re;
529                    let k2 = t2.s.re - t1.s.re;
530                    if a_mag + 10 < t0.mag_def
531                        && a_mag + 10 < t1.mag_def
532                        && b_mag + 10 < t1.mag_def
533                        && b_mag + 10 < t2.mag_def
534                    {
535                        sug = if k1 > k2 { SKIP_RIGHT } else { SKIP_LEFT };
536                    } else if sgn(a) != -sgn(b) {
537                        if a == 0.0 {
538                            sug = SKIP_LEFT;
539                        } else if b == 0.0 {
540                            sug = SKIP_RIGHT;
541                        } else if sgn(a) == sgn(t1.f_def.re) {
542                            sug = SHIFT_LEFT;
543                        } else {
544                            sug = SHIFT_RIGHT;
545                        }
546                    } else if sgn(a) == -sgn(t1.f_def.re) {
547                        new_trap = 3;
548                        sug = SYM;
549                    } else if k1 > k2 {
550                        sug = SKIP_RIGHT;
551                    } else {
552                        sug = SKIP_LEFT;
553                    }
554                } else {
555                    new_trap = 3;
556                    if sgn(a) != sgn(b) {
557                        sug = SYM;
558                    } else if a_mag > b_mag || (a_mag == b_mag && a.abs() > b.abs()) {
559                        sug = SPLIT_LEFT;
560                    } else {
561                        sug = SPLIT_RIGHT;
562                    }
563                }
564            }
565            if self.consec_moves >= 3 && self.trapped == new_trap {
566                if self.last_move == MID_LEFT || self.last_move == NEAR_RIGHT {
567                    sug = SPLIT_LEFT;
568                } else if self.last_move == MID_RIGHT || self.last_move == NEAR_LEFT {
569                    sug = SPLIT_RIGHT;
570                }
571                // (the C aborts on any other move; unreachable)
572                self.consec_moves = 0;
573            }
574            suggestion = sug;
575        }
576
577        self.trapped = new_trap;
578        suggestion
579    }
580
581    /// `PZeval`.
582    fn eval(&mut self, strat: i32, set: &Set) -> Result<Cand, Error> {
583        let seq = self.seq;
584        self.seq += 1;
585        let mut s = Complex64::new(0.0, 0.0);
586
587        match strat {
588            GUESS => {
589                if self.high_guess < self.low_guess {
590                    self.guess_param = 0.0;
591                } else if self.guess_param > 0.0 {
592                    if self.high_guess > 0.0 {
593                        self.guess_param = self.high_guess * 10.0;
594                    } else {
595                        self.guess_param = 1.0;
596                    }
597                } else if self.low_guess < 0.0 {
598                    self.guess_param = self.low_guess * 10.0;
599                } else {
600                    self.guess_param = -1.0;
601                }
602                if self.guess_param > self.high_guess {
603                    self.high_guess = self.guess_param;
604                }
605                if self.guess_param < self.low_guess {
606                    self.low_guess = self.guess_param;
607                }
608                s.re = self.guess_param;
609                s.im = set[1].map_or(0.0, |i| self.t(i).s.im);
610            }
611            SYM | SYM2 => {
612                s = self.sym2(set);
613                // Fix-ups for trapped intervals.
614                let (s0, s1, s2) = (
615                    set[0].map(|i| self.t(i).s),
616                    set[1].map(|i| self.t(i).s),
617                    set[2].map(|i| self.t(i).s),
618                );
619                if self.trapped == 1 {
620                    let (a, b) = (s0.unwrap(), s1.unwrap());
621                    if s.re < a.re || s.re > b.re {
622                        s.re = (a.re + b.re) / 2.0;
623                    }
624                } else if self.trapped == 2 {
625                    let (b, c) = (s1.unwrap(), s2.unwrap());
626                    if s.re < b.re || s.re > c.re {
627                        s.re = (b.re + c.re) / 2.0;
628                    }
629                } else if self.trapped == 3 {
630                    let (a, b, c) = (s0.unwrap(), s1.unwrap(), s2.unwrap());
631                    if s.re <= a.re || (s.re == b.re && s.im == b.im) || s.re >= c.re {
632                        s.re = (a.re + c.re) / 2.0;
633                        if s.re == b.re {
634                            if self.last_move == MID_LEFT || self.last_move == NEAR_RIGHT {
635                                s.re = (a.re + b.re) / 2.0;
636                            } else {
637                                s.re = (b.re + c.re) / 2.0;
638                            }
639                        }
640                    }
641                }
642            }
643            COMPLEX_INIT => {
644                s.re = self.t(set[1].unwrap()).s.re;
645                if self.nipzk != 0.0 && self.nipzk_mag > -10 {
646                    while self.nipzk_mag > 0 {
647                        self.nipzk *= 2.0;
648                        self.nipzk_mag -= 1;
649                    }
650                    while self.nipzk_mag < 0 {
651                        self.nipzk /= 2.0;
652                        self.nipzk_mag += 1;
653                    }
654                    s.im = self.nipzk;
655                } else {
656                    s.im = 10000.0;
657                }
658                self.nipzk = 0.0;
659                self.nipzk_mag = 0;
660            }
661            COMPLEX_GUESS => {
662                s.re = self.t(set[0].unwrap()).s.re;
663                s.im = if set[2].is_none() { 1.0e8 } else { 1.0e12 };
664            }
665            MULLER => s = self.muller(set),
666            SPLIT_LEFT => {
667                s.re = (self.t(set[0].unwrap()).s.re + 2.0 * self.t(set[1].unwrap()).s.re) / 3.0;
668            }
669            SPLIT_RIGHT => {
670                s.re = (self.t(set[2].unwrap()).s.re + 2.0 * self.t(set[1].unwrap()).s.re) / 3.0;
671            }
672            _ => return Err(Error::Build(".pz: internal — unknown step type".into())),
673        }
674
675        Ok(Cand { s, seq })
676    }
677
678    /// `NIpzSym2`: quadratic (or linear) estimate on the real axis,
679    /// with block-normalized arithmetic. Also records the minima
680    /// radical in `nipzk` (the complex-pair seed).
681    #[allow(clippy::many_single_char_names)]
682    #[allow(clippy::if_same_then_else)] // branch structure kept from the C
683    fn sym2(&mut self, set: &Set) -> Complex64 {
684        let t0 = self.t(set[0].unwrap()).clone();
685        let t1 = self.t(set[1].unwrap()).clone();
686        let t2 = self.t(set[2].unwrap()).clone();
687
688        let mut dx0 = t1.s.re - t0.s.re;
689        let mut dx1 = t2.s.re - t1.s.re;
690        let x0 = (t0.s.re + t1.s.re) / 2.0;
691        let mut d2x = (t2.s.re - t0.s.re) / 2.0;
692
693        let (mut a, mut a_mag) = zaddeq(t1.f_def.re, t1.mag_def, -t0.f_def.re, t0.mag_def);
694        let mut tmag = 0;
695        r_norm(&mut dx0, &mut tmag);
696        a /= dx0;
697        a_mag -= tmag;
698        r_norm(&mut a, &mut a_mag);
699
700        let (mut b, mut b_mag) = zaddeq(t2.f_def.re, t2.mag_def, -t1.f_def.re, t1.mag_def);
701        tmag = 0;
702        r_norm(&mut dx1, &mut tmag);
703        b /= dx1;
704        b_mag -= tmag;
705        r_norm(&mut b, &mut b_mag);
706
707        let (mut c, mut c_mag) = zaddeq(b, b_mag, -a, a_mag);
708        tmag = 0;
709        r_norm(&mut d2x, &mut tmag);
710        c /= d2x;
711        c_mag -= tmag;
712        r_norm(&mut c, &mut c_mag);
713
714        let mut out = Complex64::new(0.0, 0.0);
715
716        // The C's condition contains `(b = 0.0 || c_mag < b_mag-40)`
717        // — an assignment that clobbers `b` with 1.0/0.0 whenever
718        // the first operand of `&&` was true. Ported literally.
719        let first = a == 0.0 || c_mag < a_mag - 40;
720        let second = if first {
721            b = if c_mag < b_mag - 40 { 1.0 } else { 0.0 };
722            b != 0.0
723        } else {
724            false
725        };
726        if c == 0.0 || (first && second) {
727            // Linear path.
728            if a == 0.0 {
729                a = b;
730                a_mag = b_mag;
731            }
732            if a != 0.0 {
733                out.re = -t1.f_def.re / a;
734                let mut m = a_mag - t1.mag_def;
735                // (the C subtracts mag_def from a_mag and scales by
736                // the result)
737                while m > 0 {
738                    out.re /= 2.0;
739                    m -= 1;
740                }
741                while m < 0 {
742                    out.re *= 2.0;
743                    m += 1;
744                }
745                out.re += t1.s.re;
746            } else {
747                out.re = t1.s.re;
748            }
749        } else {
750            // Quadratic power series about set[1].s.re.
751            a /= c;
752            r_norm(&mut a, &mut a_mag);
753            a_mag -= c_mag;
754
755            let mut diff = t1.s.re - x0;
756            tmag = 0;
757            r_norm(&mut diff, &mut tmag);
758            let (na, na_mag) = zaddeq(a, a_mag, diff, tmag);
759            a = na;
760            a_mag = na_mag;
761
762            let mut bq = 2.0 * t1.f_def.re / c;
763            let mut bq_mag = t1.mag_def - c_mag;
764            r_norm(&mut bq, &mut bq_mag);
765
766            let mut disc = a * a;
767            let mut disc_mag = 2 * a_mag;
768            let (nd, nd_mag) = zaddeq(disc, disc_mag, -bq, bq_mag);
769            disc = nd;
770            disc_mag = nd_mag;
771
772            // (`new_mag` is uninitialized in the C when the
773            // discriminant is non-negative; 0 is the sane reading)
774            let mut new_mag = 0;
775            if disc < 0.0 {
776                disc = -disc;
777                new_mag = 1;
778            }
779            if disc_mag % 2 == 0 {
780                disc = disc.sqrt();
781            } else {
782                disc = (2.0 * disc).sqrt();
783                disc_mag -= 1;
784            }
785            disc_mag /= 2;
786
787            if new_mag != 0 {
788                if self.nipzk == 0.0 {
789                    self.nipzk = disc;
790                    self.nipzk_mag = disc_mag;
791                }
792                disc = 0.0;
793                disc_mag = 0;
794            }
795
796            let (mut cc, mut cc_mag);
797            if a * disc >= 0.0 {
798                let r = zaddeq(a, a_mag, disc, disc_mag);
799                cc = r.0;
800                cc_mag = r.1;
801            } else {
802                let r = zaddeq(a, a_mag, -disc, disc_mag);
803                cc = r.0;
804                cc_mag = r.1;
805            }
806
807            // Second root = C / (first root).
808            if cc != 0.0 {
809                bq /= cc;
810                bq_mag -= cc_mag;
811            } else {
812                bq = 0.0;
813                // (the C's `b = 0;` typo leaves b_mag unchanged)
814            }
815
816            let (mut rb, mut rb_mag) = zaddeq(t1.s.re, 0, -bq, bq_mag);
817            let r = zaddeq(t1.s.re, 0, -cc, cc_mag);
818            cc = r.0;
819            cc_mag = r.1;
820
821            while rb_mag > 0 {
822                rb *= 2.0;
823                rb_mag -= 1;
824            }
825            while rb_mag < 0 {
826                rb /= 2.0;
827                rb_mag += 1;
828            }
829            while cc_mag > 0 {
830                cc *= 2.0;
831                cc_mag -= 1;
832            }
833            while cc_mag < 0 {
834                cc /= 2.0;
835                cc_mag += 1;
836            }
837
838            let in_range = |x: f64| x >= t0.s.re && x <= t2.s.re;
839            if !in_range(rb) {
840                if !in_range(cc) {
841                    if self.trapped == 1 {
842                        out.re = (t0.s.re + t1.s.re) / 2.0;
843                    } else if self.trapped == 2 {
844                        out.re = (t1.s.re + t2.s.re) / 2.0;
845                    } else if self.trapped == 3 {
846                        if (t1.s.re - cc).abs() < (t1.s.re - rb).abs() {
847                            out.re = (t1.s.re + cc) / 2.0;
848                        } else {
849                            out.re = (t1.s.re + rb) / 2.0;
850                        }
851                    } else {
852                        // "Lost numerical stability" — fall back to
853                        // the midpoint rather than aborting.
854                        out.re = (t0.s.re + t2.s.re) / 2.0;
855                    }
856                } else {
857                    out.re = cc;
858                }
859            } else if !in_range(cc) {
860                out.re = rb;
861            } else if a > 0.0 {
862                out.re = rb;
863            } else {
864                out.re = cc;
865            }
866        }
867
868        out.im = 0.0;
869        out
870    }
871
872    /// `NIpzMuller`.
873    fn muller(&mut self, set: &Set) -> Complex64 {
874        let t: Vec<Trial> = (0..3).map(|i| self.t(set[i].unwrap()).clone()).collect();
875
876        let mut min = -999999i32;
877        let mut j = 0i32;
878        let mut total = 0i32;
879        for ti in &t {
880            if ti.f_def.re != 0.0 || ti.f_def.im != 0.0 {
881                if min < ti.mag_def - 50 {
882                    min = ti.mag_def - 50;
883                }
884                total += ti.mag_def;
885                j += 1;
886            }
887        }
888        let mut magx = if j != 0 { total / j } else { 0 };
889        if magx < min {
890            magx = min;
891        }
892
893        let mut scale = [1.0f64; 3];
894        for i in 0..3 {
895            let mut mag = t[i].mag_def - magx;
896            scale[i] = 1.0;
897            while mag > 0 {
898                scale[i] *= 2.0;
899                mag -= 1;
900            }
901            if mag < -90 {
902                scale[i] = 0.0;
903            } else {
904                while mag < 0 {
905                    scale[i] /= 2.0;
906                    mag += 1;
907                }
908            }
909        }
910
911        let h0 = t[0].s - t[1].s;
912        let h1 = t[1].s - t[2].s;
913        // `C_DIVEQ` divides by the conjugate form — ordinary complex
914        // division.
915        let lambda_i = h0 / h1;
916        let delta_i = lambda_i + 1.0;
917
918        let f0 = scale[0] * t[0].f_def;
919        let f1 = scale[1] * t[1].f_def;
920        let f2 = scale[2] * t[2].f_def;
921
922        let mut a = (f2 * lambda_i - f1 * delta_i + f0) * lambda_i;
923        let mut b = f2 * lambda_i * lambda_i - f1 * delta_i * delta_i + f0 * (lambda_i + delta_i);
924        let mut c = delta_i * f0;
925
926        while a.re.abs() > 1.0
927            || a.im.abs() > 1.0
928            || b.re.abs() > 1.0
929            || b.im.abs() > 1.0
930            || c.re.abs() > 1.0
931            || c.im.abs() > 1.0
932        {
933            a /= 2.0;
934            b /= 2.0;
935            c /= 2.0;
936        }
937
938        let mut d = b * b - 4.0 * a * c;
939        c_sqrt(&mut d);
940
941        // Maximize the denominator.
942        let q = b.re * d.re + b.im * d.im;
943        if q > 0.0 {
944            b += d;
945        } else {
946            b -= d;
947        }
948
949        let lambda = c / (-0.5 * b);
950        t[0].s + h0 * lambda
951    }
952
953    /// `CKTpzRunTrial`: deflation, shifting, the determinant
954    /// evaluation, and insertion into the ordered list. Returns the
955    /// index of the trial that now represents this point.
956    fn run_trial(&mut self, cand: Cand, set: &Set) -> Result<usize, Error> {
957        let mut s = cand.s;
958        if s.im < 0.0 {
959            s.im = -s.im;
960        }
961
962        let mut pretest = false;
963        let mut shifted = false;
964        let mut def_mag: i32;
965        let mut def_frac: Complex64;
966        let mut prev: Option<usize>;
967        let mut matched: Option<usize>;
968        let mut hit: Option<usize>;
969
970        loop {
971            def_mag = 0;
972            def_frac = Complex64::new(1.0, 0.0);
973            let was_shifted = shifted;
974            shifted = false;
975            prev = None;
976            matched = None;
977            hit = None;
978
979            let mut cur = self.head;
980            while let Some(i) = cur {
981                let p = self.nodes[i].clone();
982                cur = p.next;
983
984                let diff = p.s - s;
985                if diff.re < 0.0 || (diff.re == 0.0 && diff.im < 0.0) {
986                    prev = Some(i);
987                }
988
989                let (abstol, reltol) = if p.flags & ISAROOT != 0 {
990                    (1e-5, 1e-6)
991                } else {
992                    (1e-20, 1e-12)
993                };
994
995                if diff.im == 0.0 && diff.re.abs() / (p.s.re.abs() + abstol / reltol) < reltol {
996                    if was_shifted || p.count >= 3 || !self.alter(&mut s, set[1], abstol, reltol) {
997                        self.nodes[i].count = 0;
998                        pretest = true;
999                        hit = Some(i);
1000                        break;
1001                    } else {
1002                        self.nodes[i].count += 1;
1003                    }
1004                    shifted = true;
1005                    break;
1006                } else {
1007                    if self.trapped == 0 {
1008                        self.nodes[i].count = 0;
1009                    }
1010                    if p.flags & ISAROOT != 0 {
1011                        let mut diff_mag = 0i32;
1012                        let mut df = diff;
1013                        c_norm(&mut df, &mut diff_mag);
1014                        if df.im != 0.0 {
1015                            df = Complex64::new(df.re * df.re + df.im * df.im, 0.0);
1016                            diff_mag *= 2;
1017                        }
1018                        c_norm(&mut df, &mut diff_mag);
1019                        for _ in 0..p.multiplicity {
1020                            def_frac *= df;
1021                            def_mag += diff_mag;
1022                            c_norm(&mut def_frac, &mut def_mag);
1023                        }
1024                    } else if matched.is_none() {
1025                        matched = Some(i);
1026                    }
1027                }
1028            }
1029
1030            if !shifted {
1031                break;
1032            }
1033        }
1034
1035        let mut singular = false;
1036        if pretest {
1037            let i = hit.unwrap();
1038            let hflags = self.nodes[i].flags;
1039            if hflags & ISAROOT == 0
1040                && self.trapped == 3
1041                && self.nipzk != 0.0
1042                && self.nipzk_mag > -10
1043            {
1044                self.nodes[i].flags |= ISAMINIMA;
1045                return Ok(i);
1046            } else if hflags & ISAROOT != 0 {
1047                self.nodes[i].flags |= ISAREPEAT;
1048                self.nodes[i].multiplicity += 1;
1049                return Ok(i);
1050            } else {
1051                // Regular zero, as precise as we can get it.
1052                singular = true;
1053            }
1054        }
1055
1056        // Evaluate the determinant (unless the pretest already
1057        // declared the point a root).
1058        let mut f_raw = Complex64::new(0.0, 0.0);
1059        let mut mag_raw = 0i32;
1060        if !pretest {
1061            let (det, mag) = self.eng.pz_det(self.setup, s)?;
1062            f_raw = det;
1063            mag_raw = mag;
1064        }
1065
1066        let mut trial = Trial {
1067            s,
1068            f_raw,
1069            mag_raw,
1070            f_def: f_raw,
1071            mag_def: mag_raw,
1072            multiplicity: 0,
1073            count: 0,
1074            seq: cand.seq,
1075            flags: 0,
1076            prev: None,
1077            next: None,
1078            dead: false,
1079        };
1080
1081        if singular || (trial.f_raw.re == 0.0 && trial.f_raw.im == 0.0) {
1082            trial.f_raw = Complex64::new(0.0, 0.0);
1083            trial.mag_raw = 0;
1084            trial.f_def = Complex64::new(0.0, 0.0);
1085            trial.mag_def = 0;
1086            trial.flags = ISAROOT;
1087        } else {
1088            // Deflate by the roots found so far.
1089            trial.f_def = trial.f_raw / def_frac;
1090            trial.mag_def = trial.mag_raw - def_mag;
1091            c_norm(&mut trial.f_def, &mut trial.mag_def);
1092        }
1093
1094        // Link into the ordered list.
1095        let idx = self.nodes.len();
1096        let is_root = trial.flags & ISAROOT != 0;
1097        match prev {
1098            Some(p) => {
1099                trial.next = self.nodes[p].next;
1100                trial.prev = Some(p);
1101                if let Some(nx) = self.nodes[p].next {
1102                    self.nodes[nx].prev = Some(idx);
1103                }
1104                self.nodes.push(trial);
1105                self.nodes[p].next = Some(idx);
1106            }
1107            None => {
1108                trial.next = self.head;
1109                trial.prev = None;
1110                if let Some(h) = self.head {
1111                    self.nodes[h].prev = Some(idx);
1112                } else {
1113                    self.zero_trial = Some(idx);
1114                }
1115                self.nodes.push(trial);
1116                self.head = Some(idx);
1117            }
1118        }
1119        self.ntrials += 1;
1120
1121        if !is_root {
1122            if let Some(m) = matched {
1123                self.check_flat(m, idx);
1124            } else {
1125                self.nflat = 1;
1126            }
1127        }
1128
1129        Ok(idx)
1130    }
1131
1132    /// `CKTpzVerify`: count the root, deflate the surviving trials
1133    /// and drop the ones it absorbs.
1134    fn verify(&mut self, root: usize) {
1135        self.nzeros += 1;
1136        if self.nodes[root].s.im != 0.0 {
1137            self.nzeros += 1;
1138        }
1139        self.nflat = 0;
1140
1141        if self.nodes[root].multiplicity == 0 {
1142            self.nodes[root].flags |= ISAROOT;
1143            self.nodes[root].multiplicity = 1;
1144        }
1145
1146        let root_s = self.nodes[root].s;
1147        let mut prev: Option<usize> = None;
1148        let mut cur = self.head;
1149        while let Some(i) = cur {
1150            let next = self.nodes[i].next;
1151            if self.nodes[i].flags & ISAROOT != 0 {
1152                prev = Some(i);
1153                cur = next;
1154                continue;
1155            }
1156
1157            let mut diff = root_s - self.nodes[i].s;
1158            if root_s.im != 0.0 {
1159                diff = Complex64::new(diff.re * diff.re + diff.im * diff.im, 0.0);
1160            }
1161            let tdiff = diff.re;
1162            if diff.re != 0.0 {
1163                let mut diff_mag = 0i32;
1164                c_norm(&mut diff, &mut diff_mag);
1165                diff_mag = -diff_mag;
1166                let f = self.nodes[i].f_def / diff;
1167                let mut f = f;
1168                let mut m = diff_mag;
1169                c_norm(&mut f, &mut m);
1170                self.nodes[i].f_def = f;
1171                self.nodes[i].mag_def += m;
1172            }
1173
1174            if self.nodes[i].s.im != 0.0 || tdiff.abs() / (root_s.re.abs() + 200.0) < 0.005 {
1175                // Unlink and drop.
1176                if let Some(p) = prev {
1177                    self.nodes[p].next = next;
1178                }
1179                if let Some(nx) = next {
1180                    self.nodes[nx].prev = prev;
1181                }
1182                self.ntrials -= 1;
1183                if self.zero_trial == Some(i) {
1184                    self.zero_trial = next.or(self.nodes[i].prev);
1185                }
1186                if self.head == Some(i) {
1187                    self.head = next;
1188                }
1189                self.nodes[i].dead = true;
1190            } else {
1191                if let Some(p) = prev {
1192                    self.check_flat(p, i);
1193                } else {
1194                    self.nflat = 1;
1195                }
1196                self.nodes[i].flags &= !ISAMINIMA;
1197                prev = Some(i);
1198            }
1199            cur = next;
1200        }
1201    }
1202
1203    /// `CKTpzUpdateSet`.
1204    fn update_set(&mut self, set: &mut Set, new: usize) {
1205        let mut this_move = 0;
1206        let n = self.nodes[new].clone();
1207
1208        if n.s.im != 0.0 {
1209            set[2] = set[1];
1210            set[1] = set[0];
1211            set[0] = Some(new);
1212        } else if set[1].is_none() {
1213            set[1] = Some(new);
1214        } else if set[2].is_none() && n.s.re > self.t(set[1].unwrap()).s.re {
1215            set[2] = Some(new);
1216        } else if set[0].is_none() {
1217            set[0] = Some(new);
1218        } else if n.flags & ISAMINIMA != 0 {
1219            set[1] = Some(new);
1220        } else if n.s.re < self.t(set[0].unwrap()).s.re {
1221            set[2] = set[1];
1222            set[1] = set[0];
1223            set[0] = Some(new);
1224            this_move = FAR_LEFT;
1225        } else if n.s.re < self.t(set[1].unwrap()).s.re {
1226            let t1 = self.t(set[1].unwrap());
1227            if self.trapped == 0
1228                || n.mag_def < t1.mag_def
1229                || (n.mag_def == t1.mag_def && n.f_def.re.abs() < t1.f_def.re.abs())
1230            {
1231                set[2] = set[1];
1232                set[1] = Some(new);
1233                this_move = MID_LEFT;
1234            } else {
1235                set[0] = Some(new);
1236                this_move = NEAR_LEFT;
1237            }
1238        } else if set[2].is_some() && n.s.re < self.t(set[2].unwrap()).s.re {
1239            let t1 = self.t(set[1].unwrap());
1240            if self.trapped == 0
1241                || n.mag_def < t1.mag_def
1242                || (n.mag_def == t1.mag_def && n.f_def.re.abs() < t1.f_def.re.abs())
1243            {
1244                set[0] = set[1];
1245                set[1] = Some(new);
1246                this_move = MID_RIGHT;
1247            } else {
1248                set[2] = Some(new);
1249                this_move = NEAR_RIGHT;
1250            }
1251        } else {
1252            set[0] = set[1];
1253            set[1] = set[2];
1254            set[2] = Some(new);
1255            this_move = FAR_RIGHT;
1256        }
1257
1258        if self.trapped != 0 && this_move == self.last_move {
1259            self.consec_moves += 1;
1260        } else {
1261            self.consec_moves = 0;
1262        }
1263        self.last_move = this_move;
1264    }
1265
1266    /// `alter`: nudge a candidate that landed on an existing trial.
1267    fn alter(
1268        &mut self,
1269        s: &mut Complex64,
1270        nearto: Option<usize>,
1271        abstol: f64,
1272        reltol: f64,
1273    ) -> bool {
1274        let Some(nt) = nearto else {
1275            return false;
1276        };
1277        let near = self.nodes[nt].clone();
1278
1279        let p1 = if self.trapped != 2 {
1280            let mut p1 = near.s.re;
1281            if near.flags & ISAROOT != 0 {
1282                p1 -= 1e-6 * near.s.re + 1e-5;
1283            }
1284            if let Some(pv) = near.prev {
1285                p1 += self.nodes[pv].s.re;
1286            } else {
1287                p1 -= 10.0 * (p1.abs() + 1.0);
1288            }
1289            p1 / 2.0
1290        } else {
1291            near.s.re
1292        };
1293
1294        let p2 = if self.trapped != 1 {
1295            let mut p2 = near.s.re;
1296            if near.flags & ISAROOT != 0 {
1297                p2 += 1e-6 * near.s.re + 1e-5;
1298            }
1299            if let Some(nx) = near.next {
1300                p2 += self.nodes[nx].s.re;
1301            } else {
1302                p2 += 10.0 * (p2.abs() + 1.0);
1303            }
1304            p2 / 2.0
1305        } else {
1306            near.s.re
1307        };
1308
1309        let close =
1310            |x: f64, other: f64| (x - other).abs() / (other.abs() + abstol / reltol) < reltol;
1311        let bail_prev = near.prev.is_some_and(|pv| close(p1, self.nodes[pv].s.re));
1312        let bail_next = near.next.is_some_and(|nx| close(p2, self.nodes[nx].s.re));
1313        if bail_prev || bail_next {
1314            return false;
1315        }
1316
1317        if self.trapped != 2 && near.s.re - p1 > p2 - near.s.re {
1318            s.re = p1;
1319        } else {
1320            s.re = p2;
1321        }
1322        true
1323    }
1324
1325    /// `check_flat`.
1326    fn check_flat(&mut self, a: usize, b: usize) {
1327        let ta = &self.nodes[a];
1328        let tb = &self.nodes[b];
1329        let diff_mag = ta.mag_def - tb.mag_def;
1330        if diff_mag.abs() <= 1 {
1331            let mult = match diff_mag {
1332                1 => 2.0,
1333                -1 => 0.5,
1334                _ => 1.0,
1335            };
1336            let diff = mult * ta.f_def - tb.f_def;
1337            let mag2 = diff.re * diff.re + diff.im * diff.im;
1338            if mag2 < 1.0e-20 {
1339                self.nflat += 1;
1340            }
1341        }
1342    }
1343}