Skip to main content

pedalkernel/elements/
mod.rs

1//! WDF circuit elements — re-exported from pedalkernel-rt.
2//!
3//! This module re-exports all element types, traits, and implementations from
4//! the `pedalkernel-rt` runtime crate. The canonical implementations live there
5//! (no_std compatible); this module adds model-DB-dependent lookup functions
6//! via `crate::model_lookup`.
7
8pub use pedalkernel_rt::elements::nonlinear;
9pub use pedalkernel_rt::elements::*;
10
11// ---------------------------------------------------------------------------
12// Tests
13// ---------------------------------------------------------------------------
14
15#[cfg(test)]
16#[allow(deprecated)]
17mod tests {
18    use super::*;
19    use crate::model_lookup::*;
20
21    #[test]
22    fn resistor_reflects_zero() {
23        let r = Resistor::new(1000.0);
24        assert_eq!(r.reflected(), 0.0);
25    }
26
27    #[test]
28    fn capacitor_port_resistance() {
29        let c = Capacitor::new(220e-9, 48000.0);
30        let expected = 1.0 / (2.0 * 48000.0 * 220e-9);
31        assert!((c.port_resistance() - expected).abs() < 1e-3);
32    }
33
34    #[test]
35    fn capacitor_reflects_previous_incident() {
36        let mut c = Capacitor::new(220e-9, 48000.0);
37        assert_eq!(c.reflected(), 0.0);
38        c.set_incident(0.5);
39        assert_eq!(c.reflected(), 0.5);
40    }
41
42    #[test]
43    fn inductor_port_resistance() {
44        let l = Inductor::new(0.1, 48000.0);
45        let expected = 2.0 * 48000.0 * 0.1;
46        assert!((l.port_resistance() - expected).abs() < 1e-3);
47    }
48
49    #[test]
50    fn inductor_reflects_negated_previous() {
51        let mut l = Inductor::new(0.1, 48000.0);
52        l.set_incident(0.5);
53        assert_eq!(l.reflected(), -0.5);
54    }
55
56    #[test]
57    fn voltage_source_reflection() {
58        let mut vs = VoltageSource::new(50.0);
59        vs.set_voltage(1.0);
60        assert_eq!(vs.reflected(), 2.0);
61    }
62
63    #[test]
64    fn diode_pair_clips() {
65        let mut diode = DiodePairRoot::new(DiodeModel::silicon());
66        let b = diode.process(10.0, 1000.0);
67        assert!(b.abs() < 10.0, "diode should clip large input");
68    }
69
70    #[test]
71    fn diode_pair_zero_input() {
72        let mut diode = DiodePairRoot::new(DiodeModel::silicon());
73        let b = diode.process(0.0, 1000.0);
74        assert!(b.abs() < 1e-6, "zero in → zero out");
75    }
76
77    #[test]
78    fn single_diode_asymmetric() {
79        let mut diode = DiodeRoot::new(DiodeModel::silicon());
80        let b_pos = diode.process(1.0, 1000.0);
81        let b_neg = diode.process(-1.0, 1000.0);
82        assert!(
83            (b_pos - b_neg).abs() > 1e-10,
84            "should be asymmetric: b+={b_pos}, b-={b_neg}"
85        );
86    }
87
88    #[test]
89    fn diode_pair_symmetry() {
90        let mut diode = DiodePairRoot::new(DiodeModel::silicon());
91        let rp = 10_000.0;
92
93        for a in [0.5, 1.0, 2.0, 5.0, 10.0] {
94            let b_pos = diode.process(a, rp);
95            let b_neg = diode.process(-a, rp);
96            let v_pos = (a + b_pos) / 2.0;
97            let v_neg = (-a + b_neg) / 2.0;
98
99            assert!(
100                (v_pos + v_neg).abs() < 0.01,
101                "Diode pair should be symmetric: v+={v_pos:.4}, v-={v_neg:.4}"
102            );
103        }
104    }
105
106    #[test]
107    fn diode_pair_silicon_clipping_threshold() {
108        let mut diode = DiodePairRoot::new(DiodeModel::silicon());
109        let rp = 10_000.0;
110        let a = 20.0;
111        let b = diode.process(a, rp);
112        let v = (a + b) / 2.0;
113        assert!(
114            v > 0.5 && v < 1.0,
115            "Silicon diode pair should clip at ~0.6V: v={v:.4}"
116        );
117    }
118
119    #[test]
120    fn diode_germanium_lower_threshold() {
121        let mut ge_diode = DiodePairRoot::new(DiodeModel::germanium());
122        let mut si_diode = DiodePairRoot::new(DiodeModel::silicon());
123        let rp = 10_000.0;
124
125        let a = 10.0;
126        let v_ge = (a + ge_diode.process(a, rp)) / 2.0;
127        let v_si = (a + si_diode.process(a, rp)) / 2.0;
128
129        assert!(
130            v_ge < v_si,
131            "Germanium should clip lower than silicon: Ge={v_ge:.4}, Si={v_si:.4}"
132        );
133        assert!(
134            v_ge > 0.2 && v_ge < 0.5,
135            "Germanium should clip at ~0.3V: v={v_ge:.4}"
136        );
137    }
138
139    #[test]
140    fn diode_led_higher_threshold() {
141        let mut led = DiodePairRoot::new(DiodeModel::led());
142        let mut si = DiodePairRoot::new(DiodeModel::silicon());
143        let rp = 10_000.0;
144
145        let a = 20.0;
146        let v_led = (a + led.process(a, rp)) / 2.0;
147        let v_si = (a + si.process(a, rp)) / 2.0;
148
149        assert!(
150            v_led > v_si,
151            "LED should clip higher than silicon: LED={v_led:.4}, Si={v_si:.4}"
152        );
153        assert!(
154            v_led > 1.2 && v_led < 2.5,
155            "LED should clip at ~1.7V: v={v_led:.4}"
156        );
157    }
158
159    #[test]
160    fn diode_model_parameters_reasonable() {
161        let si = DiodeModel::silicon();
162        let ge = DiodeModel::germanium();
163        let led = DiodeModel::led();
164
165        assert!(ge.is > si.is, "Ge Is should be > Si Is");
166        assert!(si.is > led.is, "Si Is should be > LED Is");
167        assert!(si.n_vt > 0.02 && si.n_vt < 0.1, "Si nVt out of range");
168        assert!(ge.n_vt > 0.02 && ge.n_vt < 0.1, "Ge nVt out of range");
169        assert!(led.n_vt > 0.02 && led.n_vt < 0.1, "LED nVt out of range");
170    }
171
172    #[test]
173    fn diode_1n914_vs_1n4148() {
174        let d914 = DiodeModel::_1n914();
175        let d4148 = DiodeModel::_1n4148();
176        assert!(d914.is != d4148.is, "1N914 and 1N4148 should differ");
177        assert!(
178            (d914.is - d4148.is).abs() < 5e-9,
179            "1N914 and 1N4148 should be similar: Is_914={}, Is_4148={}",
180            d914.is,
181            d4148.is
182        );
183    }
184
185    #[test]
186    fn diode_small_rp_stability() {
187        let mut diode = DiodePairRoot::new(DiodeModel::silicon());
188        for &rp in &[1.0, 10.0, 100.0] {
189            let b = diode.process(5.0, rp);
190            assert!(b.is_finite(), "Should converge with small Rp={rp}: b={b}");
191            let v = (5.0 + b) / 2.0;
192            assert!(
193                v > 0.0 && v < 2.0,
194                "Output should be reasonable with Rp={rp}: v={v}"
195            );
196        }
197    }
198
199    #[test]
200    fn lfo_sine_range() {
201        let mut lfo = Lfo::new(LfoWaveform::Sine, 48000.0);
202        lfo.set_depth(1.0);
203        for _ in 0..48000 {
204            let v = lfo.tick();
205            assert!(v >= -1.0 && v <= 1.0, "sine out of range: {v}");
206        }
207    }
208
209    #[test]
210    fn lfo_triangle_symmetry() {
211        let mut lfo = Lfo::new(LfoWaveform::Triangle, 48000.0);
212        lfo.set_rate(1.0);
213        let samples: Vec<f64> = (0..48000).map(|_| lfo.tick()).collect();
214        let max = samples.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
215        let min = samples.iter().cloned().fold(f64::INFINITY, f64::min);
216        assert!((max - 1.0).abs() < 0.01, "triangle max should be ~1.0");
217        assert!((min + 1.0).abs() < 0.01, "triangle min should be ~-1.0");
218    }
219
220    #[test]
221    fn lfo_square_values() {
222        let mut lfo = Lfo::new(LfoWaveform::Square, 48000.0);
223        lfo.set_rate(1.0);
224        for _ in 0..48000 {
225            let v = lfo.tick();
226            assert!(
227                (v - 1.0).abs() < 0.001 || (v + 1.0).abs() < 0.001,
228                "square should be ±1"
229            );
230        }
231    }
232
233    // JFET tests
234    #[test]
235    fn jfet_cutoff_at_pinchoff() {
236        let mut jfet = JfetRoot::new(jfet_model_by_name("2N5457"));
237        jfet.set_vgs(-3.0);
238        let ids = jfet.drain_current(5.0);
239        assert!(ids.abs() < 1e-9, "should be cutoff: ids={ids}");
240    }
241
242    #[test]
243    fn jfet_triode_region() {
244        let model = jfet_model_by_name("2N5457");
245        let mut jfet = JfetRoot::new(model);
246        jfet.set_vgs(0.0);
247        let b = jfet.process(0.1, 100.0);
248        assert!(b.abs() < 0.1, "JFET triode should pass small signal");
249    }
250
251    #[test]
252    fn jfet_saturation_region() {
253        let model = jfet_model_by_name("2N5457");
254        let mut jfet = JfetRoot::new(model);
255        jfet.set_vgs(0.0);
256        let b = jfet.process(10.0, 1000.0);
257        assert!(
258            b.abs() < 10.0,
259            "JFET saturation should limit current, got b={b}"
260        );
261    }
262
263    #[test]
264    fn jfet_vgs_modulates_current() {
265        let mut jfet = JfetRoot::new(jfet_model_by_name("2N5457"));
266        jfet.set_vgs(0.0);
267        let ids_0 = jfet.drain_current(5.0);
268        jfet.set_vgs(-1.0);
269        let ids_1 = jfet.drain_current(5.0);
270        jfet.set_vgs(-2.0);
271        let ids_2 = jfet.drain_current(5.0);
272        assert!(
273            ids_0 > ids_1 && ids_1 > ids_2,
274            "Drain current should decrease as Vgs becomes more negative: ids_0={ids_0}, ids_1={ids_1}, ids_2={ids_2}"
275        );
276    }
277
278    #[test]
279    fn jfet_newton_converges() {
280        let model = jfet_model_by_name("J201");
281        let mut jfet = JfetRoot::new(model);
282        jfet.set_vgs(-0.5);
283        for a in [-10.0, -1.0, 0.0, 1.0, 10.0] {
284            let b = jfet.process(a, 1000.0);
285            assert!(b.is_finite(), "Newton should converge for a={a}");
286        }
287    }
288
289    #[test]
290    fn jfet_wdf_constraint_satisfied() {
291        let model = jfet_model_by_name("2N5457");
292        let mut jfet = JfetRoot::new(model);
293        jfet.set_vgs(-1.0);
294        let a = 2.0;
295        let rp = 1000.0;
296        let b = jfet.process(a, rp);
297        let v = (a + b) / 2.0;
298        let i = (a - b) / (2.0 * rp);
299        assert!(v.is_finite() && v.abs() < 100.0, "v should be reasonable");
300        assert!(i.is_finite() && i.abs() < 0.1, "i should be reasonable");
301    }
302
303    #[test]
304    fn jfet_p_channel_polarity() {
305        let model = jfet_model_by_name("2N5460");
306        let mut jfet = JfetRoot::new(model);
307        jfet.set_vgs(0.0);
308        let b = jfet.process(-5.0, 1000.0);
309        assert!(b.is_finite(), "P-channel should handle negative input");
310    }
311
312    // Triode tests
313    #[test]
314    fn triode_cutoff_at_negative_grid() {
315        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
316        triode.set_vgk(-50.0);
317        let ip = triode.plate_current(200.0);
318        assert!(ip < 1e-9, "should be cutoff at Vgk=-50V: ip={ip}");
319    }
320
321    #[test]
322    fn triode_active_region() {
323        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
324        triode.set_vgk(-1.5);
325        let ip = triode.plate_current(200.0);
326        assert!(
327            ip > 1e-4 && ip < 5e-3,
328            "should be in active region with realistic 12AX7 current (0.1-5mA): ip={ip} A"
329        );
330    }
331
332    #[test]
333    fn triode_plate_current_increases_with_vpk() {
334        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
335        triode.set_vgk(-1.0);
336        let ip_100 = triode.plate_current(100.0);
337        let ip_200 = triode.plate_current(200.0);
338        let ip_300 = triode.plate_current(300.0);
339        assert!(
340            ip_100 < ip_200 && ip_200 < ip_300,
341            "Ip should increase with Vpk: {ip_100} < {ip_200} < {ip_300}"
342        );
343    }
344
345    #[test]
346    fn triode_vgk_modulates_current() {
347        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
348        triode.set_vgk(0.0);
349        let ip_0 = triode.plate_current(200.0);
350        triode.set_vgk(-1.0);
351        let ip_1 = triode.plate_current(200.0);
352        triode.set_vgk(-2.0);
353        let ip_2 = triode.plate_current(200.0);
354        assert!(
355            ip_0 > ip_1 && ip_1 > ip_2,
356            "Ip should decrease as Vgk becomes more negative: ip_0={ip_0}, ip_1={ip_1}, ip_2={ip_2}"
357        );
358    }
359
360    #[test]
361    fn triode_newton_converges() {
362        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
363        triode.set_vgk(-1.5);
364        for a in [-10.0, 0.0, 10.0, 100.0, 500.0] {
365            let b = triode.process(a, 100_000.0);
366            assert!(b.is_finite(), "Newton should converge for a={a}, got b={b}");
367        }
368    }
369
370    #[test]
371    fn triode_12ax7_high_gain() {
372        let model = triode_model_by_name("12AX7");
373        assert_eq!(model.mu, 100.0, "12AX7 should have mu=100");
374    }
375
376    #[test]
377    fn triode_12au7_low_gain() {
378        let model = triode_model_by_name("12AU7");
379        assert!(
380            (model.mu - 21.5).abs() < 0.01,
381            "12AU7 should have mu=21.5 (Koren)"
382        );
383    }
384
385    #[test]
386    fn triode_different_tubes_different_current() {
387        let mut ax7 = TriodeRoot::new(triode_model_by_name("12AX7"));
388        let mut au7 = TriodeRoot::new(triode_model_by_name("12AU7"));
389        ax7.set_vgk(-1.0);
390        au7.set_vgk(-1.0);
391
392        let ip_ax7 = ax7.plate_current(200.0);
393        let ip_au7 = au7.plate_current(200.0);
394
395        assert!(
396            (ip_ax7 - ip_au7).abs() > 1e-6,
397            "Different tubes should have different Ip: ax7={ip_ax7}, au7={ip_au7}"
398        );
399    }
400
401    #[test]
402    fn triode_wdf_constraint_satisfied() {
403        let mut triode = TriodeRoot::new(triode_model_by_name("12AX7"));
404        triode.set_vgk(-1.5);
405        let a = 100.0;
406        let rp = 100_000.0;
407        let b = triode.process(a, rp);
408        let v = (a + b) / 2.0;
409        let i = (a - b) / (2.0 * rp);
410        assert!(
411            v.is_finite() && v.abs() < 1000.0,
412            "v should be reasonable: {v}"
413        );
414        assert!(
415            i.is_finite() && i.abs() < 0.01,
416            "i should be reasonable: {i}"
417        );
418    }
419
420    // Photocoupler tests
421    #[test]
422    fn photocoupler_dark_state() {
423        let pc = Photocoupler::new(PhotocouplerModel::vtl5c3(), 48000.0);
424        assert!(
425            (pc.port_resistance() - 1_000_000.0).abs() < 1.0,
426            "dark resistance should be R_dark: {}",
427            pc.port_resistance()
428        );
429    }
430
431    #[test]
432    fn photocoupler_model_presets() {
433        let vtl5c3 = PhotocouplerModel::vtl5c3();
434        let vtl5c1 = PhotocouplerModel::vtl5c1();
435        let nsl32 = PhotocouplerModel::nsl32();
436        assert!(vtl5c1.tau_fast_rise < vtl5c3.tau_fast_rise);
437        assert!(nsl32.r_dark > vtl5c3.r_dark);
438    }
439
440    // Op-amp tests
441    #[test]
442    fn opamp_unity_gain_buffer() {
443        let model = OpAmpModel::tl072();
444        let mut root = OpAmpRoot::unity_gain(model);
445        root.set_vp(1.0);
446        let rp = 10_000.0;
447        let a = 0.0;
448        let b = root.process(a, rp);
449        let v = (a + b) / 2.0;
450        assert!(
451            (v - 1.0).abs() < 0.1,
452            "Unity-gain buffer should output ≈ Vp: v={v}, Vp=1.0"
453        );
454    }
455
456    #[test]
457    fn opamp_model_presets() {
458        let tl072 = OpAmpModel::tl072();
459        let lm308 = OpAmpModel::lm308();
460        let jrc4558 = OpAmpModel::jrc4558();
461        let ne5532 = OpAmpModel::ne5532();
462        assert!(lm308.slew_rate < jrc4558.slew_rate);
463        assert!(lm308.slew_rate < tl072.slew_rate);
464        assert!(tl072.slew_rate > jrc4558.slew_rate);
465        assert!(ne5532.gbw > jrc4558.gbw);
466        assert!(tl072.open_loop_gain >= 100_000.0);
467        assert!(lm308.open_loop_gain >= 100_000.0);
468    }
469
470    #[test]
471    fn opamp_wdf_convergence() {
472        let model = OpAmpModel::jrc4558();
473        let mut root = OpAmpRoot::unity_gain(model);
474        for &vp in &[0.0, 0.1, 0.5, 1.0, -1.0, 5.0, -5.0, 10.0, -10.0] {
475            root.set_vp(vp);
476            for &a in &[0.0, 0.1, 1.0, -1.0, 10.0, -10.0] {
477                let b = root.process(a, 10_000.0);
478                assert!(
479                    b.is_finite(),
480                    "Op-amp should converge for Vp={vp}, a={a}: b={b}"
481                );
482            }
483        }
484    }
485}