Skip to main content

dune_core/
disto.rs

1//! `.disto` — small-signal distortion analysis (`ckt/distoan.c`,
2//! `ckt/cktdisto.c`, `ckt/dkerproc.c`, `ckt/dloadfns.c`,
3//! `ni/niditer.c`; Roychowdhury 1988).
4//!
5//! A Volterra-series method around the DC operating point: the
6//! first-order kernels `H1(f1)`, `H1(f2)` are ordinary AC solves
7//! driven by the sources' `DISTOF1`/`DISTOF2` inputs; each
8//! higher-order kernel is one more complex solve of the same
9//! linearized MNA system, at the combination frequency, driven by
10//! the device nonlinearities evaluated on the lower-order kernels
11//! (`DEVdisto`: Taylor coefficients of every nonlinear branch at the
12//! operating point, contracted with the kernels by the `D1n*`/`D1i*`
13//! multinomial helpers below). `DkerProc` then rescales the kernels
14//! to sinusoid amplitudes.
15//!
16//! Without `F2OVERF1` the sweep yields the 2nd and 3rd harmonics of
17//! f1; with it, the intermodulation products at f1+f2, f1−f2 and
18//! 2f1−f2 (f2 held fixed at `F2OVERF1·FSTART`, as the C does to stay
19//! compatible with hbsim).
20
21use crate::ac::{AcSweep, ac_columns};
22use crate::circuit::Circuit;
23use crate::engine::{AcStepType, Engine};
24use crate::error::Error;
25use crate::matrix::Var;
26use crate::options::Options;
27use crate::solution::Solution;
28use dune_raw::Data;
29
30/// Result of a `.disto` analysis: two plots (`DISTORTION - 2nd
31/// harmonic`, `DISTORTION - 3rd harmonic`) without `F2OVERF1`,
32/// three (`DISTORTION - IM: f1+f2`, `… f1-f2`, `… 2f1-f2`) with it.
33#[derive(Clone, Debug)]
34pub struct Disto {
35    pub solutions: Vec<Solution>,
36    pub notes: Vec<String>,
37}
38
39/// The `CKTdisto` modes that load device nonlinearities
40/// (`distodef.h`: `D_TWOF1` … `D_2F1MF2`).
41#[derive(Clone, Copy, Debug, PartialEq, Eq)]
42pub(crate) enum DistoMode {
43    /// `D_TWOF1`: second-order kernel at (f1, f1).
44    TwoF1,
45    /// `D_THRF1`: third-order kernel at (f1, f1, f1).
46    ThrF1,
47    /// `D_F1PF2`: second-order kernel at (f1, f2).
48    F1pF2,
49    /// `D_F1MF2`: second-order kernel at (f1, −f2).
50    F1mF2,
51    /// `D_2F1MF2`: third-order kernel at (f1, f1, −f2).
52    TwoF1mF2,
53}
54
55impl DistoMode {
56    /// `DkerProc`: kernel → sinusoid-amplitude factor.
57    pub(crate) fn ker_factor(self) -> f64 {
58        match self {
59            DistoMode::TwoF1 | DistoMode::ThrF1 => 2.0,
60            DistoMode::F1pF2 | DistoMode::F1mF2 => 4.0,
61            DistoMode::TwoF1mF2 => 6.0,
62        }
63    }
64}
65
66/// Which source input `CKTdisto` stamps (`D_RHSF1` / `D_RHSF2`).
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub(crate) enum DistoInput {
69    F1,
70    F2,
71}
72
73/// The operating-point environment of `DEVdisto(D_SETUP)`
74/// (`CKTrhsOld` holds the converged DC solution).
75pub(crate) struct DistoSetupCtx<'a> {
76    pub sol: &'a [f64],
77    /// `CKTgmin`.
78    pub gmin: f64,
79}
80
81/// The kernels computed so far at the current f1 (`job->r1H1ptr`
82/// …): node-indexed real/imaginary parts, `[0]` = ground = 0. A
83/// kernel not yet computed at this point is an empty slice.
84pub(crate) struct DistoKernels<'a> {
85    pub r1h1: &'a [f64],
86    pub i1h1: &'a [f64],
87    pub r2h11: &'a [f64],
88    pub i2h11: &'a [f64],
89    pub r1h2: &'a [f64],
90    pub i1h2: &'a [f64],
91    pub r2h1m2: &'a [f64],
92    pub i2h1m2: &'a [f64],
93}
94
95/// What `DEVdisto` sees in the load modes: the mode, `CKTomega` (the
96/// combination frequency being solved), the kernels and the RHS it
97/// accumulates into (`CKTrhs`/`CKTirhs`).
98pub(crate) struct DistoCtx<'a> {
99    pub mode: DistoMode,
100    pub omega: f64,
101    /// `job->Domega1`: 2π·f1 at this point (the BJT's excess-phase
102    /// delay rotates the kernels by it).
103    pub omega1: f64,
104    /// `job->Domega2`: 2π·f2 (0 without `F2OVERF1`).
105    pub omega2: f64,
106    pub h: DistoKernels<'a>,
107    pub rhs_re: &'a mut [f64],
108    pub rhs_im: &'a mut [f64],
109}
110
111impl DistoKernels<'_> {
112    /// Branch value `x = v(a) − v(b)` of a kernel pair.
113    pub(crate) fn branch(re: &[f64], im: &[f64], a: Var, b: Var) -> (f64, f64) {
114        (re[a] - re[b], im[a] - im[b])
115    }
116
117    /// The kernels of branch `v(a) − v(b)` that the devices' `disto`
118    /// routines read in `mode` (`r1h1x = r1H1[a] − r1H1[b]` …), in
119    /// the `DpassStr` layout: `h2` is H1(f2) for `D_F1PF2` and the
120    /// conjugate H1(−f2) for `D_F1MF2`/`D_2F1MF2` (the C's
121    /// `i1hm2x = −(…)`); `h11` is H2(f1,f1) for `D_THRF1`/`D_2F1MF2`;
122    /// `h12` is H2(f1,−f2) for `D_2F1MF2`. Kernels a mode does not
123    /// read stay zero.
124    pub(crate) fn kern(&self, mode: DistoMode, a: Var, b: Var) -> Kern {
125        let mut k = Kern::default();
126        (k.r1h1, k.i1h1) = Self::branch(self.r1h1, self.i1h1, a, b);
127        match mode {
128            DistoMode::TwoF1 => {}
129            DistoMode::ThrF1 => {
130                (k.r2h11, k.i2h11) = Self::branch(self.r2h11, self.i2h11, a, b);
131            }
132            DistoMode::F1pF2 => {
133                (k.r1h2, k.i1h2) = Self::branch(self.r1h2, self.i1h2, a, b);
134            }
135            DistoMode::F1mF2 => {
136                let (r, i) = Self::branch(self.r1h2, self.i1h2, a, b);
137                (k.r1h2, k.i1h2) = (r, -i);
138            }
139            DistoMode::TwoF1mF2 => {
140                let (r, i) = Self::branch(self.r1h2, self.i1h2, a, b);
141                (k.r1h2, k.i1h2) = (r, -i);
142                (k.r2h11, k.i2h11) = Self::branch(self.r2h11, self.i2h11, a, b);
143                (k.h2f1f2, k.ih2f1f2) = Self::branch(self.r2h1m2, self.i2h1m2, a, b);
144            }
145        }
146        k
147    }
148}
149
150/// The lower-order kernels of one controlling variable, named as in
151/// `DpassStr` (`r1h1x`, `i1h1x`, `r1h2x`, …, `h2f1f2x`, `ih2f1f2x`)
152/// without the variable suffix. What `h2`/`h11`/`h12` hold depends
153/// on the mode (see [`DistoKernels::kern`]).
154#[derive(Clone, Copy, Debug, Default, PartialEq)]
155pub(crate) struct Kern {
156    pub r1h1: f64,
157    pub i1h1: f64,
158    pub r1h2: f64,
159    pub i1h2: f64,
160    pub r2h11: f64,
161    pub i2h11: f64,
162    pub h2f1f2: f64,
163    pub ih2f1f2: f64,
164}
165
166impl Kern {
167    /// Field-wise sum (the C's `r1h1z = r1h1z + r1h1y` …).
168    pub(crate) fn add(&self, o: &Kern) -> Kern {
169        Kern {
170            r1h1: self.r1h1 + o.r1h1,
171            i1h1: self.i1h1 + o.i1h1,
172            r1h2: self.r1h2 + o.r1h2,
173            i1h2: self.i1h2 + o.i1h2,
174            r2h11: self.r2h11 + o.r2h11,
175            i2h11: self.i2h11 + o.i2h11,
176            h2f1f2: self.h2f1f2 + o.h2f1f2,
177            ih2f1f2: self.ih2f1f2 + o.ih2f1f2,
178        }
179    }
180
181    /// Field-wise difference (the MOS loads' `r1h1x − r1h1z` …).
182    pub(crate) fn sub(&self, o: &Kern) -> Kern {
183        Kern {
184            r1h1: self.r1h1 - o.r1h1,
185            i1h1: self.i1h1 - o.i1h1,
186            r1h2: self.r1h2 - o.r1h2,
187            i1h2: self.i1h2 - o.i1h2,
188            r2h11: self.r2h11 - o.r2h11,
189            i2h11: self.i2h11 - o.i2h11,
190            h2f1f2: self.h2f1f2 - o.h2f1f2,
191            ih2f1f2: self.ih2f1f2 - o.ih2f1f2,
192        }
193    }
194}
195
196/// The 16 Taylor coefficients of a three-variable nonlinearity, as
197/// the first half of `DpassStr` (`cxx` … `cxyz`; second order
198/// `cxx, cyy, czz, cxy, cyz, cxz`, third order `cxxx … cxyz`).
199#[derive(Clone, Copy, Debug, Default, PartialEq)]
200pub(crate) struct DfCoeffs {
201    pub cxx: f64,
202    pub cyy: f64,
203    pub czz: f64,
204    pub cxy: f64,
205    pub cyz: f64,
206    pub cxz: f64,
207    pub cxxx: f64,
208    pub cyyy: f64,
209    pub czzz: f64,
210    pub cxxy: f64,
211    pub cxxz: f64,
212    pub cxyy: f64,
213    pub cyyz: f64,
214    pub cxzz: f64,
215    pub cyzz: f64,
216    pub cxyz: f64,
217}
218
219/// `DFn2F1`.
220pub(crate) fn dfn2f1(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
221    s2v2f1(c.cxx, x.r1h1, x.i1h1, x.r1h1, x.i1h1)
222        + s2v2f1(c.cyy, y.r1h1, y.i1h1, y.r1h1, y.i1h1)
223        + s2v2f1(c.czz, z.r1h1, z.i1h1, z.r1h1, z.i1h1)
224        + s2v2f1(c.cxy, x.r1h1, x.i1h1, y.r1h1, y.i1h1)
225        + s2v2f1(c.cyz, y.r1h1, y.i1h1, z.r1h1, z.i1h1)
226        + s2v2f1(c.cxz, x.r1h1, x.i1h1, z.r1h1, z.i1h1)
227}
228
229/// `DFi2F1`.
230pub(crate) fn dfi2f1(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
231    s2i2f1(c.cxx, x.r1h1, x.i1h1, x.r1h1, x.i1h1)
232        + s2i2f1(c.cyy, y.r1h1, y.i1h1, y.r1h1, y.i1h1)
233        + s2i2f1(c.czz, z.r1h1, z.i1h1, z.r1h1, z.i1h1)
234        + s2i2f1(c.cxy, x.r1h1, x.i1h1, y.r1h1, y.i1h1)
235        + s2i2f1(c.cyz, y.r1h1, y.i1h1, z.r1h1, z.i1h1)
236        + s2i2f1(c.cxz, x.r1h1, x.i1h1, z.r1h1, z.i1h1)
237}
238
239/// `S2v3F1`/`S2i3F1` of variables `a`, `b`.
240fn s23(cab: f64, a: &Kern, b: &Kern, imag: bool) -> f64 {
241    let f = if imag { s2i3f1 } else { s2v3f1 };
242    f(
243        cab, a.r1h1, a.i1h1, b.r1h1, b.i1h1, a.r2h11, a.i2h11, b.r2h11, b.i2h11,
244    )
245}
246
247/// `S3v3F1`/`S3i3F1` of variables `a`, `b`, `c`.
248fn s33(cabc: f64, a: &Kern, b: &Kern, c: &Kern, imag: bool) -> f64 {
249    let f = if imag { s3i3f1 } else { s3v3f1 };
250    f(cabc, a.r1h1, a.i1h1, b.r1h1, b.i1h1, c.r1h1, c.i1h1)
251}
252
253/// `DFn3F1` (`imag = false`) / `DFi3F1` (`imag = true`), the C's
254/// partial sums kept in order.
255fn df3f1(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern, imag: bool) -> f64 {
256    if !imag {
257        let mut temp = s23(c.cxx, x, x, false) + s23(c.cyy, y, y, false) + s23(c.czz, z, z, false);
258        temp += s23(c.cxy, x, y, false)
259            + s23(c.cyz, y, z, false)
260            + s23(c.cxz, x, z, false)
261            + s33(c.cxxx, x, x, x, false);
262        temp += s33(c.cyyy, y, y, y, false)
263            + s33(c.czzz, z, z, z, false)
264            + s33(c.cxxy, x, x, y, false)
265            + s33(c.cxxz, x, x, z, false)
266            + s33(c.cxyy, x, y, y, false);
267        temp += s33(c.cyyz, y, y, z, false)
268            + s33(c.cxzz, x, z, z, false)
269            + s33(c.cyzz, y, z, z, false)
270            + s33(c.cxyz, x, y, z, false);
271        temp
272    } else {
273        let mut temp = s23(c.cxx, x, x, true)
274            + s23(c.cyy, y, y, true)
275            + s23(c.czz, z, z, true)
276            + s23(c.cxy, x, y, true);
277        temp += s23(c.cyz, y, z, true)
278            + s23(c.cxz, x, z, true)
279            + s33(c.cxxx, x, x, x, true)
280            + s33(c.cyyy, y, y, y, true);
281        temp += s33(c.czzz, z, z, z, true)
282            + s33(c.cxxy, x, x, y, true)
283            + s33(c.cxxz, x, x, z, true)
284            + s33(c.cxyy, x, y, y, true);
285        temp += s33(c.cyyz, y, y, z, true)
286            + s33(c.cxzz, x, z, z, true)
287            + s33(c.cyzz, y, z, z, true)
288            + s33(c.cxyz, x, y, z, true);
289        temp
290    }
291}
292
293/// `DFn3F1`.
294pub(crate) fn dfn3f1(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
295    df3f1(c, x, y, z, false)
296}
297
298/// `DFi3F1`.
299pub(crate) fn dfi3f1(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
300    df3f1(c, x, y, z, true)
301}
302
303/// `S2vF12`/`S2iF12` of variables `a`, `b`.
304fn s2f12(cab: f64, a: &Kern, b: &Kern, imag: bool) -> f64 {
305    let f = if imag { s2if12 } else { s2vf12 };
306    f(
307        cab, a.r1h1, a.i1h1, b.r1h1, b.i1h1, a.r1h2, a.i1h2, b.r1h2, b.i1h2,
308    )
309}
310
311/// `DFnF12` (`imag = false`) / `DFiF12`, halved like the C.
312fn dff12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern, imag: bool) -> f64 {
313    let mut temp = s2f12(c.cxx, x, x, imag) + s2f12(c.cyy, y, y, imag) + s2f12(c.czz, z, z, imag);
314    temp += s2f12(c.cxy, x, y, imag) + s2f12(c.cyz, y, z, imag) + s2f12(c.cxz, x, z, imag);
315    0.5 * temp
316}
317
318/// `DFnF12`.
319pub(crate) fn dfnf12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
320    dff12(c, x, y, z, false)
321}
322
323/// `DFiF12`.
324pub(crate) fn dfif12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
325    dff12(c, x, y, z, true)
326}
327
328/// `S2v2F12`/`S2i2F12` of variables `a`, `b`.
329fn s22f12(cab: f64, a: &Kern, b: &Kern, imag: bool) -> f64 {
330    let f = if imag { s2i2f12 } else { s2v2f12 };
331    f(
332        cab, a.r1h1, a.i1h1, b.r1h1, b.i1h1, a.r1h2, a.i1h2, b.r1h2, b.i1h2, a.r2h11, a.i2h11,
333        b.r2h11, b.i2h11, a.h2f1f2, a.ih2f1f2, b.h2f1f2, b.ih2f1f2,
334    )
335}
336
337/// `S3v2F12`/`S3i2F12` of variables `a`, `b`, `c`.
338fn s32f12(cabc: f64, a: &Kern, b: &Kern, c: &Kern, imag: bool) -> f64 {
339    let f = if imag { s3i2f12 } else { s3v2f12 };
340    f(
341        cabc, a.r1h1, a.i1h1, b.r1h1, b.i1h1, c.r1h1, c.i1h1, a.r1h2, a.i1h2, b.r1h2, b.i1h2,
342        c.r1h2, c.i1h2,
343    )
344}
345
346/// `DFn2F12` (`imag = false`) / `DFi2F12` (the C's `DpassStr`
347/// functions), the partial sums in the C's order, divided by 3.
348fn df2f12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern, imag: bool) -> f64 {
349    let i = imag;
350    let mut temp = s22f12(c.cxx, x, x, i);
351    temp += s22f12(c.cyy, y, y, i);
352    temp += s22f12(c.czz, z, z, i);
353    temp += s22f12(c.cxy, x, y, i);
354    temp += s22f12(c.cyz, y, z, i);
355    temp += s22f12(c.cxz, x, z, i);
356    if !imag {
357        temp += s32f12(c.cxxx, x, x, x, i) + s32f12(c.cyyy, y, y, y, i);
358        temp += s32f12(c.czzz, z, z, z, i) + s32f12(c.cxxy, x, x, y, i);
359        temp += s32f12(c.cxxz, x, x, z, i) + s32f12(c.cxyy, x, y, y, i);
360        temp += s32f12(c.cyyz, y, y, z, i) + s32f12(c.cxzz, x, z, z, i);
361        temp += s32f12(c.cyzz, y, z, z, i) + s32f12(c.cxyz, x, y, z, i);
362    } else {
363        temp += s32f12(c.cxxx, x, x, x, i);
364        temp += s32f12(c.cyyy, y, y, y, i) + s32f12(c.czzz, z, z, z, i);
365        temp += s32f12(c.cxxy, x, x, y, i) + s32f12(c.cxxz, x, x, z, i);
366        temp += s32f12(c.cxyy, x, y, y, i) + s32f12(c.cyyz, y, y, z, i);
367        temp += s32f12(c.cxzz, x, z, z, i);
368        temp += s32f12(c.cyzz, y, z, z, i) + s32f12(c.cxyz, x, y, z, i);
369    }
370    temp / 3.
371}
372
373/// `DFn2F12`.
374pub(crate) fn dfn2f12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
375    df2f12(c, x, y, z, false)
376}
377
378/// `DFi2F12`.
379pub(crate) fn dfi2f12(c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> f64 {
380    df2f12(c, x, y, z, true)
381}
382
383/// The `(temp, itemp)` a device's `disto` routine computes for a
384/// three-variable memoryless nonlinearity in `mode`: `DFn2F1`/
385/// `DFi2F1`, `DFn3F1`/`DFi3F1`, `DFnF12`/`DFiF12` (both IM
386/// second-order modes — the caller's kernels already hold H1(f2) or
387/// its conjugate) or `DFn2F12`/`DFi2F12`.
388pub(crate) fn df_term(mode: DistoMode, c: &DfCoeffs, x: &Kern, y: &Kern, z: &Kern) -> (f64, f64) {
389    match mode {
390        DistoMode::TwoF1 => (dfn2f1(c, x, y, z), dfi2f1(c, x, y, z)),
391        DistoMode::ThrF1 => (dfn3f1(c, x, y, z), dfi3f1(c, x, y, z)),
392        DistoMode::F1pF2 | DistoMode::F1mF2 => (dfnf12(c, x, y, z), dfif12(c, x, y, z)),
393        DistoMode::TwoF1mF2 => (dfn2f12(c, x, y, z), dfi2f12(c, x, y, z)),
394    }
395}
396
397/// The one-variable counterpart of [`df_term`]: `D1n2F1`/`D1i2F1`,
398/// `D1n3F1`/`D1i3F1`, `D1nF12`/`D1iF12`, `D1n2F12`/`D1i2F12` with
399/// second- and third-order coefficients `cxx`, `cxxx`.
400pub(crate) fn d1_term(mode: DistoMode, cxx: f64, cxxx: f64, x: &Kern) -> (f64, f64) {
401    match mode {
402        DistoMode::TwoF1 => (d1n2f1(cxx, x.r1h1, x.i1h1), d1i2f1(cxx, x.r1h1, x.i1h1)),
403        DistoMode::ThrF1 => (
404            d1n3f1(cxx, cxxx, x.r1h1, x.i1h1, x.r2h11, x.i2h11),
405            d1i3f1(cxx, cxxx, x.r1h1, x.i1h1, x.r2h11, x.i2h11),
406        ),
407        DistoMode::F1pF2 | DistoMode::F1mF2 => (
408            d1nf12(cxx, x.r1h1, x.i1h1, x.r1h2, x.i1h2),
409            d1if12(cxx, x.r1h1, x.i1h1, x.r1h2, x.i1h2),
410        ),
411        DistoMode::TwoF1mF2 => (
412            d1n2f12(
413                cxx, cxxx, x.r1h1, x.i1h1, x.r1h2, x.i1h2, x.r2h11, x.i2h11, x.h2f1f2, x.ih2f1f2,
414            ),
415            d1i2f12(
416                cxx, cxxx, x.r1h1, x.i1h1, x.r1h2, x.i1h2, x.r2h11, x.i2h11, x.h2f1f2, x.ih2f1f2,
417            ),
418        ),
419    }
420}
421
422/// A charge term's current: `jω` times the charge's `(temp, itemp)`
423/// — the C's `temp = −ω·D…i(…)`, `itemp = ω·D…n(…)`.
424pub(crate) fn jomega(omega: f64, (n, i): (f64, f64)) -> (f64, f64) {
425    (-omega * i, omega * n)
426}
427
428impl DistoCtx<'_> {
429    /// Stamp a nonlinear branch current flowing from `p` to `n`
430    /// (`CKTrhs[p] −= temp; CKTrhs[n] += temp`, same for `CKTirhs`).
431    pub(crate) fn stamp(&mut self, p: Var, n: Var, (temp, itemp): (f64, f64)) {
432        self.rhs_re[p] -= temp;
433        self.rhs_im[p] -= itemp;
434        self.rhs_re[n] += temp;
435        self.rhs_im[n] += itemp;
436    }
437}
438
439// ---------------------------------------------------------------
440// `ckt/dloadfns.c`: the multinomial sub-functions (`S…`) and the
441// one-variable load functions (`D1…`). Names follow the C's
442// renaming convention (`2v` = two-variable term `xy`, `2F12` = two
443// F1 plus F2, `v` = real part, `i` = imaginary part), lowercased.
444// ---------------------------------------------------------------
445
446/// `S2v2F1`.
447pub(crate) fn s2v2f1(cxy: f64, r1h1x: f64, i1h1x: f64, r1h1y: f64, i1h1y: f64) -> f64 {
448    cxy * (r1h1x * r1h1y - i1h1x * i1h1y)
449}
450
451/// `S2i2F1`.
452pub(crate) fn s2i2f1(cxy: f64, r1h1x: f64, i1h1x: f64, r1h1y: f64, i1h1y: f64) -> f64 {
453    cxy * (r1h1x * i1h1y + i1h1x * r1h1y)
454}
455
456/// `S2v3F1`.
457#[allow(clippy::too_many_arguments)]
458pub(crate) fn s2v3f1(
459    cxy: f64,
460    r1h1x: f64,
461    i1h1x: f64,
462    r1h1y: f64,
463    i1h1y: f64,
464    r2h11x: f64,
465    i2h11x: f64,
466    r2h11y: f64,
467    i2h11y: f64,
468) -> f64 {
469    cxy * (r1h1x * r2h11y - i1h1x * i2h11y + r1h1y * r2h11x - i1h1y * i2h11x)
470}
471
472/// `S2i3F1`.
473#[allow(clippy::too_many_arguments)]
474pub(crate) fn s2i3f1(
475    cxy: f64,
476    r1h1x: f64,
477    i1h1x: f64,
478    r1h1y: f64,
479    i1h1y: f64,
480    r2h11x: f64,
481    i2h11x: f64,
482    r2h11y: f64,
483    i2h11y: f64,
484) -> f64 {
485    cxy * (r1h1x * i2h11y + i1h1x * r2h11y + r1h1y * i2h11x + i1h1y * r2h11x)
486}
487
488/// `S2vF12`.
489#[allow(clippy::too_many_arguments)]
490pub(crate) fn s2vf12(
491    cxy: f64,
492    r1h1x: f64,
493    i1h1x: f64,
494    r1h1y: f64,
495    i1h1y: f64,
496    r1h2x: f64,
497    i1h2x: f64,
498    r1h2y: f64,
499    i1h2y: f64,
500) -> f64 {
501    cxy * (r1h1x * r1h2y - i1h1x * i1h2y + r1h1y * r1h2x - i1h1y * i1h2x)
502}
503
504/// `S2iF12`.
505#[allow(clippy::too_many_arguments)]
506pub(crate) fn s2if12(
507    cxy: f64,
508    r1h1x: f64,
509    i1h1x: f64,
510    r1h1y: f64,
511    i1h1y: f64,
512    r1h2x: f64,
513    i1h2x: f64,
514    r1h2y: f64,
515    i1h2y: f64,
516) -> f64 {
517    cxy * (r1h1x * i1h2y + i1h1x * r1h2y + r1h1y * i1h2x + i1h1y * r1h2x)
518}
519
520/// `S2v2F12` (17 arguments in the C).
521#[allow(clippy::too_many_arguments)]
522pub(crate) fn s2v2f12(
523    cxy: f64,
524    r1h1x: f64,
525    i1h1x: f64,
526    r1h1y: f64,
527    i1h1y: f64,
528    r1h2x: f64,
529    i1h2x: f64,
530    r1h2y: f64,
531    i1h2y: f64,
532    r2h11x: f64,
533    i2h11x: f64,
534    r2h11y: f64,
535    i2h11y: f64,
536    h2f1f2x: f64,
537    ih2f1f2x: f64,
538    h2f1f2y: f64,
539    ih2f1f2y: f64,
540) -> f64 {
541    cxy * (2.0 * (r1h1x * h2f1f2y - i1h1x * ih2f1f2y + r1h1y * h2f1f2x - i1h1y * ih2f1f2x)
542        + r1h2x * r2h11y
543        - i1h2x * i2h11y
544        + r1h2y * r2h11x
545        - i1h2y * i2h11x)
546}
547
548/// `S2i2F12` (17 arguments in the C).
549#[allow(clippy::too_many_arguments)]
550pub(crate) fn s2i2f12(
551    cxy: f64,
552    r1h1x: f64,
553    i1h1x: f64,
554    r1h1y: f64,
555    i1h1y: f64,
556    r1h2x: f64,
557    i1h2x: f64,
558    r1h2y: f64,
559    i1h2y: f64,
560    r2h11x: f64,
561    i2h11x: f64,
562    r2h11y: f64,
563    i2h11y: f64,
564    h2f1f2x: f64,
565    ih2f1f2x: f64,
566    h2f1f2y: f64,
567    ih2f1f2y: f64,
568) -> f64 {
569    cxy * (2.0 * (r1h1x * ih2f1f2y + i1h1x * h2f1f2y + r1h1y * ih2f1f2x + i1h1y * h2f1f2x)
570        + r1h2x * i2h11y
571        + i1h2x * r2h11y
572        + r1h2y * i2h11x
573        + i1h2y * r2h11x)
574}
575
576/// `S3v3F1`.
577#[allow(clippy::too_many_arguments)]
578pub(crate) fn s3v3f1(
579    cxyz: f64,
580    r1h1x: f64,
581    i1h1x: f64,
582    r1h1y: f64,
583    i1h1y: f64,
584    r1h1z: f64,
585    i1h1z: f64,
586) -> f64 {
587    cxyz * ((r1h1x * r1h1y - i1h1x * i1h1y) * r1h1z - (i1h1x * r1h1y + r1h1x * i1h1y) * i1h1z)
588}
589
590/// `S3i3F1`.
591#[allow(clippy::too_many_arguments)]
592pub(crate) fn s3i3f1(
593    cxyz: f64,
594    r1h1x: f64,
595    i1h1x: f64,
596    r1h1y: f64,
597    i1h1y: f64,
598    r1h1z: f64,
599    i1h1z: f64,
600) -> f64 {
601    cxyz * ((r1h1x * r1h1y - i1h1x * i1h1y) * i1h1z + (i1h1x * r1h1y + r1h1x * i1h1y) * r1h1z)
602}
603
604/// `S3v2F12` (13 arguments in the C).
605#[allow(clippy::too_many_arguments)]
606pub(crate) fn s3v2f12(
607    cxyz: f64,
608    r1h1x: f64,
609    i1h1x: f64,
610    r1h1y: f64,
611    i1h1y: f64,
612    r1h1z: f64,
613    i1h1z: f64,
614    r1h2x: f64,
615    i1h2x: f64,
616    r1h2y: f64,
617    i1h2y: f64,
618    r1h2z: f64,
619    i1h2z: f64,
620) -> f64 {
621    cxyz * ((r1h1x * r1h1y - i1h1x * i1h1y) * r1h2z - (i1h1x * r1h1y + r1h1x * i1h1y) * i1h2z
622        + (r1h1x * r1h1z - i1h1x * i1h1z) * r1h2y
623        - (i1h1x * r1h1z + r1h1x * i1h1z) * i1h2y
624        + (r1h1z * r1h1y - i1h1z * i1h1y) * r1h2x
625        - (i1h1z * r1h1y + r1h1z * i1h1y) * i1h2x)
626}
627
628/// `S3i2F12` (13 arguments in the C).
629#[allow(clippy::too_many_arguments)]
630pub(crate) fn s3i2f12(
631    cxyz: f64,
632    r1h1x: f64,
633    i1h1x: f64,
634    r1h1y: f64,
635    i1h1y: f64,
636    r1h1z: f64,
637    i1h1z: f64,
638    r1h2x: f64,
639    i1h2x: f64,
640    r1h2y: f64,
641    i1h2y: f64,
642    r1h2z: f64,
643    i1h2z: f64,
644) -> f64 {
645    cxyz * ((r1h1x * r1h1y - i1h1x * i1h1y) * i1h2z
646        + (i1h1x * r1h1y + r1h1x * i1h1y) * r1h2z
647        + (r1h1x * r1h1z - i1h1x * i1h1z) * i1h2y
648        + (i1h1x * r1h1z + r1h1x * i1h1z) * r1h2y
649        + (r1h1z * r1h1y - i1h1z * i1h1y) * i1h2x
650        + (i1h1z * r1h1y + r1h1z * i1h1y) * r1h2x)
651}
652
653/// `D1n2F1`: real part of `cxx·H1(f1)²`.
654pub(crate) fn d1n2f1(cxx: f64, r1h1x: f64, i1h1x: f64) -> f64 {
655    s2v2f1(cxx, r1h1x, i1h1x, r1h1x, i1h1x)
656}
657
658/// `D1i2F1`: imaginary part of `cxx·H1(f1)²`.
659pub(crate) fn d1i2f1(cxx: f64, r1h1x: f64, i1h1x: f64) -> f64 {
660    s2i2f1(cxx, r1h1x, i1h1x, r1h1x, i1h1x)
661}
662
663/// `D1n3F1`: real part of `2·cxx·H1·H2(f1,f1) + cxxx·H1³`.
664pub(crate) fn d1n3f1(cxx: f64, cxxx: f64, r1h1x: f64, i1h1x: f64, r2h11x: f64, i2h11x: f64) -> f64 {
665    s2v3f1(
666        cxx, r1h1x, i1h1x, r1h1x, i1h1x, r2h11x, i2h11x, r2h11x, i2h11x,
667    ) + s3v3f1(cxxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h1x, i1h1x)
668}
669
670/// `D1i3F1`.
671pub(crate) fn d1i3f1(cxx: f64, cxxx: f64, r1h1x: f64, i1h1x: f64, r2h11x: f64, i2h11x: f64) -> f64 {
672    s2i3f1(
673        cxx, r1h1x, i1h1x, r1h1x, i1h1x, r2h11x, i2h11x, r2h11x, i2h11x,
674    ) + s3i3f1(cxxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h1x, i1h1x)
675}
676
677/// `D1nF12`: real part of `cxx·H1(f1)·H1(f2)`.
678pub(crate) fn d1nf12(cxx: f64, r1h1x: f64, i1h1x: f64, r1h2x: f64, i1h2x: f64) -> f64 {
679    let temp = s2vf12(cxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x);
680    0.5 * temp
681}
682
683/// `D1iF12`.
684pub(crate) fn d1if12(cxx: f64, r1h1x: f64, i1h1x: f64, r1h2x: f64, i1h2x: f64) -> f64 {
685    let temp = s2if12(cxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x);
686    0.5 * temp
687}
688
689/// `D1n2F12`: real part of the third-order kernel at (f1, f1, f2)
690/// (divided by 3: "otherwise we get 3*kernel").
691#[allow(clippy::too_many_arguments)]
692pub(crate) fn d1n2f12(
693    cxx: f64,
694    cxxx: f64,
695    r1h1x: f64,
696    i1h1x: f64,
697    r1h2x: f64,
698    i1h2x: f64,
699    r2h11x: f64,
700    i2h11x: f64,
701    h2f1f2x: f64,
702    ih2f1f2x: f64,
703) -> f64 {
704    let temp = s2v2f12(
705        cxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x, r2h11x, i2h11x, r2h11x,
706        i2h11x, h2f1f2x, ih2f1f2x, h2f1f2x, ih2f1f2x,
707    ) + s3v2f12(
708        cxxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x, r1h2x, i1h2x,
709    );
710    temp / 3.
711}
712
713/// `D1i2F12`.
714#[allow(clippy::too_many_arguments)]
715pub(crate) fn d1i2f12(
716    cxx: f64,
717    cxxx: f64,
718    r1h1x: f64,
719    i1h1x: f64,
720    r1h2x: f64,
721    i1h2x: f64,
722    r2h11x: f64,
723    i2h11x: f64,
724    h2f1f2x: f64,
725    ih2f1f2x: f64,
726) -> f64 {
727    let temp = s2i2f12(
728        cxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x, r2h11x, i2h11x, r2h11x,
729        i2h11x, h2f1f2x, ih2f1f2x, h2f1f2x, ih2f1f2x,
730    ) + s3i2f12(
731        cxxx, r1h1x, i1h1x, r1h1x, i1h1x, r1h1x, i1h1x, r1h2x, i1h2x, r1h2x, i1h2x, r1h2x, i1h2x,
732    );
733    temp / 3.
734}
735
736impl Circuit {
737    /// Run `.disto DEC|OCT|LIN NP FSTART FSTOP <F2OVERF1>`
738    /// (`DISTOan`). The sources' `DISTOF1`/`DISTOF2` values drive
739    /// the first-order kernels; their `AC` values play no part.
740    pub fn disto(
741        &self,
742        opts: &Options,
743        sweep: AcSweep,
744        points: usize,
745        fstart: f64,
746        fstop: f64,
747        f2overf1: Option<f64>,
748    ) -> Result<Disto, Error> {
749        // `DsetParm`: "Frequency of 0 is invalid".
750        let ok = fstart > 0.0 && fstart.is_finite() && fstop > 0.0 && fstop.is_finite();
751        if !ok {
752            return Err(Error::Build(".disto frequencies are inconsistent".into()));
753        }
754        if f2overf1.is_some_and(|r| !r.is_finite()) {
755            return Err(Error::Build(".disto F2OVERF1 is not finite".into()));
756        }
757        let built = self.build(opts)?;
758        let notes = built.notes.clone();
759        let mut engine = Engine::new(built);
760        let step_type = match sweep {
761            AcSweep::Decade => AcStepType::Decade,
762            AcSweep::Octave => AcStepType::Octave,
763            AcSweep::Linear => AcStepType::Linear,
764        };
765        let plots = engine.disto(opts, step_type, points, fstart, fstop, f2overf1)?;
766        let solutions = plots
767            .into_iter()
768            .map(|(plotname, rows)| {
769                let (variables, columns) = ac_columns(&engine.vars, &rows);
770                Solution {
771                    plotname: plotname.to_string(),
772                    variables,
773                    data: Data::Complex(columns),
774                    scale: Some(0),
775                }
776            })
777            .collect();
778        Ok(Disto { solutions, notes })
779    }
780}
781
782#[cfg(test)]
783mod tests {
784    use super::*;
785    use num_complex::Complex64;
786
787    fn c(re: f64, im: f64) -> Complex64 {
788        Complex64::new(re, im)
789    }
790
791    fn close(a: f64, b: f64) -> bool {
792        (a - b).abs() <= 1e-12 * (1.0 + a.abs().max(b.abs()))
793    }
794
795    /// The real/imaginary helper pairs are the parts of plain
796    /// complex products: `D1·2F1 = c·x²`, `D1·F12 = c·x·y`, `D1·3F1
797    /// = 2c₂·x·H2 + c₃·x³`, `D1·2F12 = (2c₂(2x·H2(1,2) + y·H2(1,1))
798    /// + 3c₃·x²·y)/3`.
799    #[test]
800    fn dloadfns_are_complex_multinomials() {
801        let (cxx, cxxx) = (0.37, -1.9);
802        let x = c(0.8, -0.3);
803        let y = c(-0.2, 0.65);
804        let h11 = c(1.1, 0.4);
805        let h12 = c(-0.7, 0.25);
806
807        let v = cxx * x * x;
808        assert!(close(d1n2f1(cxx, x.re, x.im), v.re));
809        assert!(close(d1i2f1(cxx, x.re, x.im), v.im));
810
811        let v = 0.5 * cxx * (2.0 * x * y);
812        assert!(close(d1nf12(cxx, x.re, x.im, y.re, y.im), v.re));
813        assert!(close(d1if12(cxx, x.re, x.im, y.re, y.im), v.im));
814
815        let v = cxx * 2.0 * x * h11 + cxxx * x * x * x;
816        assert!(close(d1n3f1(cxx, cxxx, x.re, x.im, h11.re, h11.im), v.re));
817        assert!(close(d1i3f1(cxx, cxxx, x.re, x.im, h11.re, h11.im), v.im));
818
819        let v = (cxx * (2.0 * (2.0 * x * h12) + 2.0 * y * h11) + cxxx * 3.0 * x * x * y) / 3.0;
820        let n = d1n2f12(
821            cxx, cxxx, x.re, x.im, y.re, y.im, h11.re, h11.im, h12.re, h12.im,
822        );
823        let i = d1i2f12(
824            cxx, cxxx, x.re, x.im, y.re, y.im, h11.re, h11.im, h12.re, h12.im,
825        );
826        assert!(close(n, v.re), "{n} vs {}", v.re);
827        assert!(close(i, v.im), "{i} vs {}", v.im);
828    }
829
830    /// `S3v2F12`/`S3i2F12` are the symmetric three-variable product
831    /// `x·y·w + x·z·v + z·y·u` (u, v, w the f2 kernels of x, y, z).
832    #[test]
833    fn three_variable_f12_term_is_symmetric_product() {
834        let (x, y, z) = (c(0.3, 0.1), c(-0.5, 0.2), c(0.7, -0.4));
835        let (u, v, w) = (c(0.2, 0.9), c(-0.1, -0.3), c(0.6, 0.05));
836        let want = 1.7 * (x * y * w + x * z * v + z * y * u);
837        let args = (
838            x.re, x.im, y.re, y.im, z.re, z.im, u.re, u.im, v.re, v.im, w.re, w.im,
839        );
840        let re = s3v2f12(
841            1.7, args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
842            args.10, args.11,
843        );
844        let im = s3i2f12(
845            1.7, args.0, args.1, args.2, args.3, args.4, args.5, args.6, args.7, args.8, args.9,
846            args.10, args.11,
847        );
848        assert!(close(re, want.re) && close(im, want.im));
849    }
850
851    fn kern(h1: Complex64, h2: Complex64, h11: Complex64, h12: Complex64) -> Kern {
852        Kern {
853            r1h1: h1.re,
854            i1h1: h1.im,
855            r1h2: h2.re,
856            i1h2: h2.im,
857            r2h11: h11.re,
858            i2h11: h11.im,
859            h2f1f2: h12.re,
860            ih2f1f2: h12.im,
861        }
862    }
863
864    /// The three-variable load functions (`DFn*`/`DFi*`) are the
865    /// parts of the multinomial expansions of `Σ c_ab·a·b +
866    /// Σ c_abc·a·b·c` over the kernels: `2F1` = Σ c_ab x_a x_b,
867    /// `3F1` = Σ c_ab (x_a h_b + x_b h_a) + Σ c_abc x_a x_b x_c,
868    /// `F12` = ½ Σ c_ab (x_a u_b + x_b u_a), `2F12` = ⅓ [Σ c_ab
869    /// (2(x_a w_b + x_b w_a) + u_a h_b + u_b h_a) + Σ c_abc (x_a x_b
870    /// u_c + x_a x_c u_b + x_c x_b u_a)] with u the f2 kernel, h =
871    /// H2(f1,f1), w = H2(f1,−f2) — each coefficient checked alone.
872    #[test]
873    fn df_loads_are_three_variable_multinomials() {
874        let x = [c(0.8, -0.3), c(-0.2, 0.65), c(0.45, 0.1)];
875        let u = [c(0.3, 0.2), c(-0.6, -0.15), c(0.05, 0.9)];
876        let h = [c(1.1, 0.4), c(-0.7, 0.25), c(0.2, -0.8)];
877        let w = [c(-0.4, 0.3), c(0.9, 0.1), c(-0.25, -0.55)];
878        let k: Vec<Kern> = (0..3).map(|i| kern(x[i], u[i], h[i], w[i])).collect();
879
880        // (coefficient setter, index tuple)
881        type Set = fn(&mut DfCoeffs, f64);
882        let second: [(Set, usize, usize); 6] = [
883            (|c, v| c.cxx = v, 0, 0),
884            (|c, v| c.cyy = v, 1, 1),
885            (|c, v| c.czz = v, 2, 2),
886            (|c, v| c.cxy = v, 0, 1),
887            (|c, v| c.cyz = v, 1, 2),
888            (|c, v| c.cxz = v, 0, 2),
889        ];
890        let third: [(Set, usize, usize, usize); 10] = [
891            (|c, v| c.cxxx = v, 0, 0, 0),
892            (|c, v| c.cyyy = v, 1, 1, 1),
893            (|c, v| c.czzz = v, 2, 2, 2),
894            (|c, v| c.cxxy = v, 0, 0, 1),
895            (|c, v| c.cxxz = v, 0, 0, 2),
896            (|c, v| c.cxyy = v, 0, 1, 1),
897            (|c, v| c.cyyz = v, 1, 1, 2),
898            (|c, v| c.cxzz = v, 0, 2, 2),
899            (|c, v| c.cyzz = v, 1, 2, 2),
900            (|c, v| c.cxyz = v, 0, 1, 2),
901        ];
902        let cv = 0.73;
903        let check = |mode: DistoMode, co: &DfCoeffs, want: Complex64| {
904            let (n, i) = df_term(mode, co, &k[0], &k[1], &k[2]);
905            assert!(
906                close(n, want.re) && close(i, want.im),
907                "{mode:?}: ({n}, {i}) vs {want}"
908            );
909        };
910        for (set, a, b) in second {
911            let mut co = DfCoeffs::default();
912            set(&mut co, cv);
913            check(DistoMode::TwoF1, &co, cv * x[a] * x[b]);
914            check(DistoMode::ThrF1, &co, cv * (x[a] * h[b] + x[b] * h[a]));
915            check(
916                DistoMode::F1pF2,
917                &co,
918                0.5 * cv * (x[a] * u[b] + x[b] * u[a]),
919            );
920            let v = cv * (2.0 * (x[a] * w[b] + x[b] * w[a]) + u[a] * h[b] + u[b] * h[a]) / 3.0;
921            check(DistoMode::TwoF1mF2, &co, v);
922        }
923        for (set, a, b, d) in third {
924            let mut co = DfCoeffs::default();
925            set(&mut co, cv);
926            check(DistoMode::TwoF1, &co, c(0.0, 0.0));
927            check(DistoMode::ThrF1, &co, cv * x[a] * x[b] * x[d]);
928            check(DistoMode::F1mF2, &co, c(0.0, 0.0));
929            let v = cv * (x[a] * x[b] * u[d] + x[a] * x[d] * u[b] + x[d] * x[b] * u[a]) / 3.0;
930            check(DistoMode::TwoF1mF2, &co, v);
931        }
932    }
933
934    /// The one-variable dispatcher is the `D1*` family, and `jω×`
935    /// is a complex multiplication by `jω`.
936    #[test]
937    fn d1_term_and_jomega() {
938        let k = kern(c(0.8, -0.3), c(0.3, 0.2), c(1.1, 0.4), c(-0.4, 0.3));
939        let (n, i) = d1_term(DistoMode::ThrF1, 0.37, -1.9, &k);
940        assert_eq!(n, d1n3f1(0.37, -1.9, k.r1h1, k.i1h1, k.r2h11, k.i2h11));
941        assert_eq!(i, d1i3f1(0.37, -1.9, k.r1h1, k.i1h1, k.r2h11, k.i2h11));
942        let z = c(0.3, -0.7);
943        let (re, im) = jomega(2.5, (z.re, z.im));
944        let want = c(0.0, 2.5) * z;
945        assert!(close(re, want.re) && close(im, want.im));
946    }
947
948    fn divider(distof2: Option<(f64, f64)>) -> Circuit {
949        use crate::circuit::{Resistor, Vsource};
950        let mut ckt = Circuit::new("divider");
951        ckt.add(Vsource {
952            name: "v1".into(),
953            nodes: ["1".into(), "0".into()],
954            dc: Some(1.0),
955            wave: None,
956            ac: None,
957            distof1: Some((0.1, 0.0)),
958            distof2,
959            port: None,
960        })
961        .unwrap();
962        for (name, a, b) in [("r1", "1", "2"), ("r2", "2", "0")] {
963            ckt.add(Resistor {
964                name: name.into(),
965                nodes: [a.into(), b.into()],
966                value: 1e3,
967            })
968            .unwrap();
969        }
970        ckt
971    }
972
973    /// A linear circuit has no distortion: every higher-order kernel
974    /// is identically zero, and the plots follow `DISTOan`'s naming.
975    #[test]
976    fn linear_circuit_has_zero_kernels() {
977        let opts = Options::default();
978        let d = divider(None)
979            .disto(&opts, AcSweep::Decade, 2, 1e3, 1e4, None)
980            .unwrap();
981        let names: Vec<&str> = d.solutions.iter().map(|s| s.plotname.as_str()).collect();
982        assert_eq!(
983            names,
984            ["DISTORTION - 2nd harmonic", "DISTORTION - 3rd harmonic"]
985        );
986        for sol in &d.solutions {
987            assert_eq!(sol.num_points(), 3);
988            for v in sol.column_complex("v(2)").unwrap() {
989                assert_eq!(*v, Complex64::new(0.0, 0.0));
990            }
991        }
992        let d = divider(Some((0.1, 0.0)))
993            .disto(&opts, AcSweep::Linear, 1, 1e3, 2e3, Some(0.9))
994            .unwrap();
995        assert_eq!(d.solutions.len(), 3);
996        assert_eq!(d.solutions[2].plotname, "DISTORTION - IM: 2f1-f2");
997    }
998
999    /// `E_NOF2SRC`: F2OVERF1 given but no source has a DISTOF2 input.
1000    #[test]
1001    fn f2_wanted_without_f2_source_fails() {
1002        let err = divider(None)
1003            .disto(&Options::default(), AcSweep::Decade, 1, 1e3, 1e3, Some(0.5))
1004            .unwrap_err();
1005        assert!(err.to_string().contains("f2"), "{err}");
1006    }
1007
1008    #[test]
1009    fn dkerproc_factors() {
1010        assert_eq!(DistoMode::TwoF1.ker_factor(), 2.0);
1011        assert_eq!(DistoMode::ThrF1.ker_factor(), 2.0);
1012        assert_eq!(DistoMode::F1pF2.ker_factor(), 4.0);
1013        assert_eq!(DistoMode::F1mF2.ker_factor(), 4.0);
1014        assert_eq!(DistoMode::TwoF1mF2.ker_factor(), 6.0);
1015    }
1016}