1pub use pedalkernel_rt::elements::nonlinear;
9pub use pedalkernel_rt::elements::*;
10
11#[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 #[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 #[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 #[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 #[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}