Skip to main content

dune_core/
sp.rs

1//! `.sp` — ngspice's S-parameter analysis (`analysis/span.c`,
2//! `cktspdum.c`, the RFSPICE parts of `vsrc/` and `nevalsrc.c`;
3//! Alessio Cacciatori 2021). It postdates the 3f5, so ngspice-47 is
4//! both the contract and the oracle.
5//!
6//! Ports are V sources carrying `PORTNUM N <Z0 Z>`: in every
7//! analysis such a source drives its `+` node through the series
8//! reference impedance `Z0` (internal node `<name>#res`). Per
9//! frequency `.sp` does one AC load with every V-source stimulus off
10//! (`MODESP`) and one complex factorization, then for each port `j`
11//! solves with 1 V on port `j`'s branch and records, for every port
12//! `i`, the Kurokawa power waves
13//!
14//! ```text
15//! a_i = (V_i + Z0_i·I_i) / (2·√Z0_i)    b_i = (V_i − Z0_i·I_i) / (2·√Z0_i)
16//! ```
17//!
18//! (`V_i` across the external terminals, `I_i` into the network) as
19//! column `j` of `A` and `B` (`CKTspCalcPowerWave`). Then
20//! `S = B·A⁻¹`, and with `Z0 = diag(Z0_i)`, `Gn = diag(1/√Z0_i)`
21//! (`CKTspCalcSMatrix`):
22//!
23//! ```text
24//! Z = Gn⁻¹ · (E − S)⁻¹ · (S·Z0 + Z0) · Gn
25//! Y = Gn⁻¹ · (S·Z0 + Z0)⁻¹ · (E − S) · Gn
26//! ```
27//!
28//! With `DONOISE = 1` the analysis adds the short-circuit noise
29//! current correlation matrix `Cy` (A²/Hz): each thermal/shot
30//! generator `(n1, n2, PSD)` of the devices is carried to the ports
31//! through the adjoint solutions (unit current into each port's `+`
32//! node, ports terminated in `Z0`), giving the terminated port
33//! voltages `v`, whose Norton currents are `i = v/Z0 + Y·v`;
34//! `Cy += i·iᴴ`. Flicker noise never reaches `Cy` (it goes through
35//! `NevalSrc`'s `N_GAIN` path, which records nothing under `.sp`).
36//! For two ports the noise parameters follow from `Cy/(4kT)`
37//! (Maas, *Noise*): `NF` and `NFmin` in dB at the port-1 reference
38//! admittance, `SOpt` and `Rn`.
39
40use dune_raw::{Data, VarType};
41use num_complex::Complex64;
42
43use crate::ac::AcSweep;
44use crate::circuit::{Circuit, PortInfo, VarKind, saved_node};
45use crate::engine::{AcStepType, Engine, MAX_OUTPUT_VALUES, sweep_budget};
46use crate::error::Error;
47use crate::options::Options;
48use crate::solution::Solution;
49
50/// Result of an S-parameter analysis: one complex plot `SP Analysis`
51/// over `frequency` holding the node voltages and branch currents of
52/// the last port's excitation, then `S_i_j`, `Y_i_j`, `Z_i_j`
53/// (`i` the responding port, `j` the driven one) and, with noise,
54/// `i(Cy_i_j)` plus, for two ports, `NF`, `SOpt`, `NFmin`, `Rn` —
55/// named and typed as ngspice-47's rawfile writes them.
56#[derive(Clone, Debug)]
57pub struct Sp {
58    pub solution: Solution,
59    pub notes: Vec<String>,
60}
61
62type CMat = Vec<Vec<Complex64>>;
63
64impl Circuit {
65    /// Run `.sp DEC|OCT|LIN N FSTART FSTOP <DONOISE>` over the
66    /// circuit's RF ports (V sources with [`crate::RfPort`]); at
67    /// least two ports are required.
68    pub fn sp(
69        &self,
70        opts: &Options,
71        sweep: AcSweep,
72        points: usize,
73        fstart: f64,
74        fstop: f64,
75        donoise: bool,
76    ) -> Result<Sp, Error> {
77        // `SPsetParm` refuses negative frequencies; `SPan` refuses a
78        // non-positive start on log sweeps.
79        if !(fstart.is_finite() && fstop.is_finite()) || fstart < 0.0 || fstop < 0.0 {
80            return Err(Error::Build(".sp frequencies are inconsistent".into()));
81        }
82        if sweep != AcSweep::Linear && fstart <= 0.0 {
83            return Err(Error::Build(".sp start frequency must be positive".into()));
84        }
85        let built = self.build(opts)?;
86        let notes = built.notes.clone();
87        let ports = built.ports.clone();
88        match ports.len() {
89            0 => {
90                return Err(Error::Build(
91                    "no RF port is present, cannot run sp analysis".into(),
92                ));
93            }
94            1 => {
95                return Err(Error::Build(
96                    "only one RF port is found, sp needs at least two".into(),
97                ));
98            }
99            _ => {}
100        }
101        let np = ports.len();
102        let mut engine = Engine::new(built);
103
104        let n = points.max(1);
105        let step_type = match sweep {
106            AcSweep::Decade => AcStepType::Decade,
107            AcSweep::Octave => AcStepType::Octave,
108            AcSweep::Linear => AcStepType::Linear,
109        };
110        // `span.c` keeps the 3f5 stepping: `10^(1/n)` per decade even
111        // where ngspice-47's `.ac` re-spaces the points onto FSTOP.
112        let freq_delta = match sweep {
113            AcSweep::Decade => f64::exp(f64::ln(10.0) / n as f64),
114            AcSweep::Octave => f64::exp(f64::ln(2.0) / n as f64),
115            AcSweep::Linear => {
116                if n - 1 > 1 {
117                    (fstop - fstart) / (n - 1) as f64
118                } else {
119                    0.0
120                }
121            }
122        };
123        let extra = np
124            .saturating_mul(np)
125            .saturating_mul(if donoise { 4 } else { 3 })
126            + 4;
127        let row_len = engine.vars.len().saturating_add(extra);
128        sweep_budget(step_type, n, fstart.max(1e-300), fstop, row_len, "sp")?;
129
130        engine.sp_prepare(opts)?;
131
132        let freq_tol = match sweep {
133            AcSweep::Linear => freq_delta * opts.reltol,
134            _ => freq_delta * fstop * opts.reltol,
135        };
136
137        let mut cols = Columns::new(&engine, np, donoise);
138        let mut freq = fstart;
139        while freq <= fstop + freq_tol {
140            if (cols.rows + 1).saturating_mul(row_len) > MAX_OUTPUT_VALUES {
141                return Err(Error::Build(
142                    "sp sweep produces too many points; refusing".into(),
143                ));
144            }
145            let (forward, adjoint) = engine.sp_point(freq, &ports, donoise)?;
146            let (a, b) = power_waves(&ports, &forward);
147            let (s, y, z) = network_matrices(&a, &b, &ports);
148            let noise = donoise.then(|| {
149                let gens = engine.sp_noise_generators(freq, opts.temp);
150                let cy = noise_correlation(&gens, &adjoint, &y, &ports);
151                let params = (np == 2).then(|| noise_params(&cy, &y, &ports, opts.temp));
152                (cy, params)
153            });
154            let last = forward.last().map(Vec::as_slice).unwrap_or(&[]);
155            cols.push(freq, last, &s, &y, &z, noise.as_ref());
156
157            match sweep {
158                AcSweep::Decade | AcSweep::Octave => {
159                    freq *= freq_delta;
160                    if freq_delta == 1.0 {
161                        break;
162                    }
163                }
164                AcSweep::Linear => {
165                    freq += freq_delta;
166                    if freq_delta == 0.0 {
167                        break;
168                    }
169                }
170            }
171        }
172
173        let (variables, columns) = cols.finish();
174        Ok(Sp {
175            solution: Solution {
176                plotname: "SP Analysis".to_string(),
177                variables,
178                data: Data::Complex(columns),
179                scale: Some(0),
180            },
181            notes,
182        })
183    }
184}
185
186/// `CKTspCalcPowerWave` over every excitation: `A[i][j]`, `B[i][j]`
187/// are port `i`'s incident and scattered waves with port `j` driven.
188/// `I_i = −I(branch)` is the current into the network (the branch
189/// current flows from the internal node through the source).
190fn power_waves(ports: &[PortInfo], forward: &[Vec<Complex64>]) -> (CMat, CMat) {
191    let np = ports.len();
192    let mut a = vec![vec![Complex64::new(0.0, 0.0); np]; np];
193    let mut b = a.clone();
194    for (col, sol) in forward.iter().enumerate() {
195        for (row, p) in ports.iter().enumerate() {
196            let ki = 0.5 / p.z0.sqrt();
197            let i = -sol[p.br];
198            let v = sol[p.pos] - sol[p.neg];
199            a[row][col] = (v + i * p.z0) * ki;
200            b[row][col] = (v - i * p.z0) * ki;
201        }
202    }
203    (a, b)
204}
205
206/// `CKTspCalcSMatrix`: S, Y and Z in the C's operation order.
207fn network_matrices(a: &CMat, b: &CMat, ports: &[PortInfo]) -> (CMat, CMat, CMat) {
208    let np = ports.len();
209    let diag = |f: &dyn Fn(&PortInfo) -> f64| -> CMat {
210        let mut m = zeros(np);
211        for (i, p) in ports.iter().enumerate() {
212            m[i][i] = Complex64::new(f(p), 0.0);
213        }
214        m
215    };
216    let zref = diag(&|p| p.z0);
217    let gn = diag(&|p| 2.0 * (0.5 / p.z0.sqrt()));
218    let gninv = diag(&|p| 1.0 / (2.0 * (0.5 / p.z0.sqrt())));
219    let eye = diag(&|_| 1.0);
220
221    let s = mul(b, &inverse(a));
222    let sz0_z0 = add(&mul(&s, &zref), &zref);
223    let e_s = sub(&eye, &s);
224    let z = mul(&gninv, &mul(&inverse(&e_s), &mul(&sz0_z0, &gn)));
225    let y = mul(&gninv, &mul(&inverse(&sz0_z0), &mul(&e_s, &gn)));
226    (s, y, z)
227}
228
229/// `NevalSrc` under `DOING_SP`, summed over every generator: the
230/// short-circuit noise-current correlation matrix (A²/Hz).
231fn noise_correlation(
232    gens: &[crate::matrix::NoiseGen],
233    adjoint: &[Vec<Complex64>],
234    y: &CMat,
235    ports: &[PortInfo],
236) -> CMat {
237    let np = ports.len();
238    let mut cy = zeros(np);
239    let mut v = vec![Complex64::new(0.0, 0.0); np];
240    let mut cur = v.clone();
241    for &(n1, n2, density) in gens {
242        let inoise = density.sqrt();
243        for (s, adj) in adjoint.iter().enumerate() {
244            v[s] = (adj[n1] - adj[n2]) * inoise;
245        }
246        for d in 0..np {
247            let mut c = v[d] * (1.0 / ports[d].z0);
248            for s in 0..np {
249                c += y[d][s] * v[s];
250            }
251            cur[d] = c;
252        }
253        for d in 0..np {
254            for s in 0..np {
255                cy[d][s] += cur[d] * cur[s].conj();
256            }
257        }
258    }
259    cy
260}
261
262/// Two-port noise parameters (`CKTspnoise`, `N_CALC`).
263#[derive(Clone, Copy, Debug)]
264struct NoiseParams {
265    /// Noise figure at the port-1 reference admittance, dB.
266    nf: f64,
267    /// Optimum source reflection coefficient.
268    sopt: Complex64,
269    /// Minimum noise figure, dB.
270    nfmin: f64,
271    /// Equivalent noise resistance, Ω.
272    rn: f64,
273}
274
275fn noise_params(cy: &CMat, y: &CMat, ports: &[PortInfo], temp: f64) -> NoiseParams {
276    let knorm = 4.0 * crate::consts::BOLTZ * temp;
277    let mut t: CMat = cy
278        .iter()
279        .map(|r| r.iter().map(|c| c * (1.0 / knorm)).collect())
280        .collect();
281    let y21mod = y[1][0].norm_sqr();
282    let mut rn = t[1][1].re / y21mod;
283    if rn.abs() < 1e-30 {
284        rn = 1e-30;
285    }
286    if t[1][1].re.abs() < 1e-30 && t[1][1].im.abs() < 1e-30 {
287        t[1][1].re = 1e-30;
288    }
289    let ycor = y[0][0] - (t[0][1] / t[1][1]) * y[1][0];
290    let y11_ycor = (y[0][0] - ycor).norm_sqr();
291    let gu = t[0][0].re - rn * y11_ycor;
292    let ysopt = Complex64::new((ycor.re * ycor.re + gu / rn).sqrt(), -ycor.im);
293    let y0 = Complex64::new(1.0 / ports[0].z0, 0.0);
294    let sopt = (y0 - ysopt) / (y0 + ysopt);
295    let fmin = 1.0 + 2.0 * rn * (ycor.re + ysopt.re);
296    let nf = fmin + (rn / y0.re) * (y0 - ysopt).norm_sqr();
297    NoiseParams {
298        nf: 10.0 * nf.log10(),
299        sopt,
300        nfmin: 10.0 * fmin.log10(),
301        rn,
302    }
303}
304
305/// The plot's columns, filled row by row (`CKTspDump`).
306struct Columns {
307    variables: Vec<(String, VarType)>,
308    /// Var index per saved node/branch column.
309    saved: Vec<usize>,
310    columns: Vec<Vec<Complex64>>,
311    np: usize,
312    rows: usize,
313}
314
315impl Columns {
316    fn new(engine: &Engine, np: usize, donoise: bool) -> Self {
317        let mut variables = vec![("frequency".to_string(), VarType::Frequency)];
318        let mut saved = Vec::new();
319        let vars = &engine.vars;
320        for i in 1..vars.len() {
321            let name = &vars.names[i];
322            match vars.kinds[i] {
323                VarKind::Node if saved_node(name) => {
324                    variables.push((format!("v({name})"), VarType::Voltage));
325                    saved.push(i);
326                }
327                VarKind::Branch => {
328                    variables.push((format!("i({name})"), VarType::Current));
329                    saved.push(i);
330                }
331                _ => {}
332            }
333        }
334        // `guess_type` in ngspice's `outitf.c` types the `.sp`
335        // vectors by prefix, and its rawfile writer wraps the
336        // current-typed `Cy_` ones in `i(…)`.
337        let matrix = |variables: &mut Vec<(String, VarType)>, prefix: &str, t: VarType| {
338            for i in 1..=np {
339                for j in 1..=np {
340                    variables.push((format!("{prefix}_{i}_{j}"), t.clone()));
341                }
342            }
343        };
344        matrix(&mut variables, "S", VarType::SParam);
345        matrix(&mut variables, "Y", VarType::Other("admittance".into()));
346        matrix(&mut variables, "Z", VarType::Other("impedance".into()));
347        if donoise {
348            for i in 1..=np {
349                for j in 1..=np {
350                    variables.push((format!("i(Cy_{i}_{j})"), VarType::Current));
351                }
352            }
353            if np == 2 {
354                variables.push(("NF".into(), VarType::Other("decibel".into())));
355                variables.push(("SOpt".into(), VarType::NoType));
356                variables.push(("NFmin".into(), VarType::Other("decibel".into())));
357                variables.push(("Rn".into(), VarType::Other("impedance".into())));
358            }
359        }
360        let columns = vec![Vec::new(); variables.len()];
361        Columns {
362            variables,
363            saved,
364            columns,
365            np,
366            rows: 0,
367        }
368    }
369
370    fn push(
371        &mut self,
372        freq: f64,
373        sol: &[Complex64],
374        s: &CMat,
375        y: &CMat,
376        z: &CMat,
377        noise: Option<&(CMat, Option<NoiseParams>)>,
378    ) {
379        let mut k = 0;
380        let mut put = |cols: &mut Vec<Vec<Complex64>>, v: Complex64| {
381            cols[k].push(v);
382            k += 1;
383        };
384        put(&mut self.columns, Complex64::new(freq, 0.0));
385        for &i in &self.saved {
386            put(&mut self.columns, sol[i]);
387        }
388        let mut mats: Vec<&CMat> = vec![s, y, z];
389        if let Some((cy, _)) = noise {
390            mats.push(cy);
391        }
392        for m in mats {
393            for row in m.iter().take(self.np) {
394                for &v in row.iter().take(self.np) {
395                    put(&mut self.columns, v);
396                }
397            }
398        }
399        if let Some((_, Some(p))) = noise {
400            put(&mut self.columns, Complex64::new(p.nf, 0.0));
401            put(&mut self.columns, p.sopt);
402            put(&mut self.columns, Complex64::new(p.nfmin, 0.0));
403            put(&mut self.columns, Complex64::new(p.rn, 0.0));
404        }
405        self.rows += 1;
406    }
407
408    fn finish(self) -> (Vec<(String, VarType)>, Vec<Vec<Complex64>>) {
409        (self.variables, self.columns)
410    }
411}
412
413fn zeros(n: usize) -> CMat {
414    vec![vec![Complex64::new(0.0, 0.0); n]; n]
415}
416
417fn mul(a: &CMat, b: &CMat) -> CMat {
418    let n = a.len();
419    let mut c = zeros(n);
420    for i in 0..n {
421        for k in 0..n {
422            let aik = a[i][k];
423            for j in 0..n {
424                c[i][j] += aik * b[k][j];
425            }
426        }
427    }
428    c
429}
430
431fn add(a: &CMat, b: &CMat) -> CMat {
432    a.iter()
433        .zip(b)
434        .map(|(r, s)| r.iter().zip(s).map(|(x, y)| x + y).collect())
435        .collect()
436}
437
438fn sub(a: &CMat, b: &CMat) -> CMat {
439    a.iter()
440        .zip(b)
441        .map(|(r, s)| r.iter().zip(s).map(|(x, y)| x - y).collect())
442        .collect()
443}
444
445/// `cinverse` (`maths/dense/dense.c`) computes `adj(A)/det(A)`; this
446/// is the same inverse by Gauss–Jordan elimination with partial
447/// pivoting (cofactor expansion is factorial in the port count). An
448/// exactly singular matrix gives all-NaN, as the C's division by a
449/// zero determinant does.
450fn inverse(a: &CMat) -> CMat {
451    let n = a.len();
452    let mut m = a.clone();
453    let mut inv = zeros(n);
454    for (i, row) in inv.iter_mut().enumerate() {
455        row[i] = Complex64::new(1.0, 0.0);
456    }
457    for col in 0..n {
458        let piv = (col..n)
459            .max_by(|&x, &y| m[x][col].norm().total_cmp(&m[y][col].norm()))
460            .unwrap_or(col);
461        if m[piv][col].norm() == 0.0 {
462            return vec![vec![Complex64::new(f64::NAN, f64::NAN); n]; n];
463        }
464        m.swap(col, piv);
465        inv.swap(col, piv);
466        let d = m[col][col].inv();
467        for j in 0..n {
468            m[col][j] *= d;
469            inv[col][j] *= d;
470        }
471        for r in 0..n {
472            if r != col {
473                let f = m[r][col];
474                if f != Complex64::new(0.0, 0.0) {
475                    for j in 0..n {
476                        let (mc, ic) = (m[col][j], inv[col][j]);
477                        m[r][j] -= f * mc;
478                        inv[r][j] -= f * ic;
479                    }
480                }
481            }
482        }
483    }
484    inv
485}
486
487#[cfg(test)]
488mod tests {
489    use super::*;
490    use crate::circuit::{Resistor, RfPort, Vsource};
491
492    fn port(name: &str, node: &str, num: u32, z0: f64) -> Vsource {
493        Vsource {
494            name: name.into(),
495            nodes: [node.into(), "0".into()],
496            dc: Some(0.0),
497            wave: None,
498            ac: None,
499            distof1: None,
500            distof2: None,
501            port: Some(RfPort { num, z0 }),
502        }
503    }
504
505    fn close(a: Complex64, b: Complex64, tol: f64) -> bool {
506        (a - b).norm() <= tol * (1.0 + b.norm())
507    }
508
509    /// A matched 50 Ω T-pad with voltage ratio K = 2 (6.02 dB):
510    /// series arms R1 = Z0·(K−1)/(K+1), shunt R2 = 2·Z0·K/(K²−1).
511    /// Closed form: S11 = S22 = 0, S21 = S12 = 1/K; Z11 = Z22 =
512    /// R1 + R2, Z12 = Z21 = R2; Y = Z⁻¹.
513    #[test]
514    fn matched_t_attenuator() {
515        let (z0, k) = (50.0, 2.0);
516        let r1 = z0 * (k - 1.0) / (k + 1.0);
517        let r2 = 2.0 * z0 * k / (k * k - 1.0);
518        let mut ckt = Circuit::new("t-pad");
519        // Port 2 first: the ports are ordered by index, not by card.
520        ckt.add(port("v2", "out", 2, z0)).unwrap();
521        ckt.add(port("v1", "in", 1, z0)).unwrap();
522        for (name, a, b, r) in [
523            ("ra", "in", "mid", r1),
524            ("rb", "mid", "out", r1),
525            ("rc", "mid", "0", r2),
526        ] {
527            ckt.add(Resistor {
528                name: name.into(),
529                nodes: [a.into(), b.into()],
530                value: r,
531            })
532            .unwrap();
533        }
534        let sp = ckt
535            .sp(&Options::default(), AcSweep::Decade, 1, 1e3, 1e6, true)
536            .unwrap();
537        let sol = &sp.solution;
538        assert_eq!(sol.plotname, "SP Analysis");
539        assert_eq!(sol.num_points(), 4);
540        let at = |name: &str| sol.column_complex(name).unwrap()[2];
541        let c = |re: f64| Complex64::new(re, 0.0);
542        for (name, want) in [
543            ("S_1_1", 0.0),
544            ("S_2_2", 0.0),
545            ("S_2_1", 1.0 / k),
546            ("S_1_2", 1.0 / k),
547            ("Z_1_1", r1 + r2),
548            ("Z_2_2", r1 + r2),
549            ("Z_1_2", r2),
550            ("Z_2_1", r2),
551        ] {
552            assert!(close(at(name), c(want), 1e-12), "{name}: {}", at(name));
553        }
554        let det = (r1 + r2) * (r1 + r2) - r2 * r2;
555        assert!(close(at("Y_1_1"), c((r1 + r2) / det), 1e-12));
556        assert!(close(at("Y_2_1"), c(-r2 / det), 1e-12));
557        // Noise: a passive network's Cy is 4kT·Re(Y) (Bosma).
558        let four_kt = 4.0 * crate::consts::BOLTZ * Options::default().temp;
559        assert!(close(at("i(Cy_1_1)"), c(four_kt * (r1 + r2) / det), 1e-9));
560        assert!(close(at("i(Cy_2_1)"), c(-four_kt * r2 / det), 1e-9));
561        // A matched passive attenuator's noise figure is its loss.
562        let nf = at("NF").re;
563        assert!((nf - 20.0 * k.log10()).abs() < 1e-9, "NF = {nf}");
564        // The node vectors are those of the last port's excitation
565        // (port 2 driven by 1 V behind 50 Ω into a matched load).
566        assert!(close(at("v(out)"), c(0.5), 1e-12));
567        assert!(close(at("v(v2#res)"), c(1.0), 1e-12));
568    }
569
570    #[test]
571    fn port_checks() {
572        let mut ckt = Circuit::new("ports");
573        ckt.add(port("v1", "a", 1, 50.0)).unwrap();
574        ckt.add(Resistor {
575            name: "r1".into(),
576            nodes: ["a".into(), "0".into()],
577            value: 50.0,
578        })
579        .unwrap();
580        let opts = Options::default();
581        let e = ckt
582            .sp(&opts, AcSweep::Linear, 2, 1.0, 2.0, false)
583            .unwrap_err();
584        assert!(e.to_string().contains("only one RF port"), "{e}");
585        ckt.add(port("v3", "a", 3, 50.0)).unwrap();
586        let e = ckt
587            .sp(&opts, AcSweep::Linear, 2, 1.0, 2.0, false)
588            .unwrap_err();
589        assert!(e.to_string().contains("v3: incorrect port ordering"), "{e}");
590        // The check runs for every analysis, as `VSRCtemp` does.
591        assert!(ckt.op(&opts).is_err());
592    }
593
594    #[test]
595    fn inverse_roundtrip_and_singular() {
596        let m: CMat = vec![
597            vec![Complex64::new(0.0, 1.0), Complex64::new(2.0, 0.0)],
598            vec![Complex64::new(3.0, -1.0), Complex64::new(0.5, 0.25)],
599        ];
600        let p = mul(&m, &inverse(&m));
601        for (i, row) in p.iter().enumerate() {
602            for (j, v) in row.iter().enumerate() {
603                let want = if i == j { 1.0 } else { 0.0 };
604                assert!(close(*v, Complex64::new(want, 0.0), 1e-14));
605            }
606        }
607        let z = zeros(2);
608        assert!(inverse(&z)[0][0].re.is_nan());
609    }
610}