Skip to main content

pedalkernel/compiler/components/
tubes.rs

1//! Vacuum tube component structs: Triode, Pentode, VariMu.
2
3use hashbrown::HashMap;
4
5use crate::compiler::classify::NonlinearKind;
6use crate::compiler::component::{
7    Component, ComponentEdge, EdgeKind, GraphRole, KMethodSpec, ModulationSink, ModulationSinkKind,
8    NeighborReq, NeighborRole, PinConfig, PinDirection, SignalTerminals, StampResult,
9    K_AXIS_INCIDENT_CONTROL_2D, K_AXIS_INCIDENT_CONTROL_CONTROL_3D,
10};
11use crate::compiler::graph::NodeId;
12use crate::tree::MnaSystem;
13
14use super::impl_component_dyn;
15
16// ═══════════════════════════════════════════════════════════════════════════
17// Triode
18// ═══════════════════════════════════════════════════════════════════════════
19
20#[derive(Debug, Clone, PartialEq)]
21pub struct Triode {
22    pub model: String,
23}
24
25impl Component for Triode {
26    impl_component_dyn!();
27
28    fn type_tag(&self) -> &'static str {
29        "triode"
30    }
31
32    fn ports(&self) -> Vec<(&'static str, &'static str)> {
33        vec![("plate", "cathode")] // grid is voltage-sense
34    }
35
36    fn signal_terminals(&self) -> SignalTerminals {
37        SignalTerminals::Amplifier {
38            input: "grid",
39            output: "plate",
40            control: None,
41        }
42    }
43
44    fn is_passive(&self) -> bool {
45        false
46    }
47
48    fn is_nonlinear(&self) -> bool {
49        true
50    }
51
52    fn pin_config(&self) -> PinConfig {
53        PinConfig {
54            valid_pins: &["plate", "cathode", "grid", "vgk"],
55            aliases: &[],
56        }
57    }
58
59    fn modulation_pins(&self) -> &'static [&'static str] {
60        &["vgk"]
61    }
62
63    fn graph_role(&self) -> GraphRole {
64        GraphRole::CoupledEdge {
65            edge_pin_a: "plate",
66            edge_pin_b: "cathode",
67            coupled_pins: &["plate", "cathode", "grid"],
68        }
69    }
70
71    fn stamp_mna(
72        &self,
73        _comp_id: &str,
74        _n1: Option<usize>,
75        _n2: Option<usize>,
76        _mna: &mut MnaSystem,
77        _sample_rate: f64,
78    ) -> StampResult {
79        StampResult::Skip
80    }
81
82    fn edges(&self) -> Vec<ComponentEdge> {
83        vec![ComponentEdge {
84            pin_a: "plate",
85            pin_b: "cathode",
86            kind: EdgeKind::Nonlinear,
87            port_group: None,
88        }]
89    }
90
91    fn classify_nonlinear(
92        &self,
93        comp_id: &str,
94        node_a: NodeId,
95        node_b: NodeId,
96        _gnd_node: NodeId,
97        node_names: &HashMap<String, NodeId>,
98    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
99        let grid_node = node_names.get(&format!("{}.grid", comp_id)).copied();
100        Some((
101            NonlinearKind::Triode {
102                model_name: self.model.clone(),
103                plate_node: node_a,
104                cathode_node: node_b,
105                grid_node,
106                parallel_count: 1,
107                is_vari_mu: false,
108            },
109            vec![node_a, node_b],
110        ))
111    }
112
113    fn modulation_sink(&self, pin: &str) -> Option<ModulationSink> {
114        match pin {
115            "vgk" => Some(ModulationSink {
116                target_kind: ModulationSinkKind::TriodeVgk,
117                bias: -2.0,
118                range: 2.0,
119            }),
120            _ => None,
121        }
122    }
123
124    fn footprint_ref(&self) -> (&'static str, &'static str) {
125        ("Valve:Triode", "V")
126    }
127
128    fn symbol_name(&self) -> &'static str {
129        "triode"
130    }
131    fn layout_class(&self) -> &'static str {
132        "triode"
133    }
134
135    fn pin_direction(&self, pin: &str) -> PinDirection {
136        match pin {
137            "grid" => PinDirection::Input,
138            "plate" => PinDirection::Output,
139            "cathode" => PinDirection::Down,
140            _ => PinDirection::Bidirectional,
141        }
142    }
143
144    fn is_tube(&self) -> bool {
145        true
146    }
147    fn is_gain_device(&self) -> bool {
148        true
149    }
150    fn k_method_spec(&self) -> Option<KMethodSpec> {
151        Some(KMethodSpec {
152            axes: K_AXIS_INCIDENT_CONTROL_2D,
153            reason: "tube: 2D memoryless I-V",
154        })
155    }
156    fn model_name(&self) -> Option<&str> {
157        Some(&self.model)
158    }
159
160    fn terminal_requirements(&self) -> Vec<(&'static str, Vec<NeighborReq>)> {
161        // Output terminals each declare a Required Load; the device-level rule
162        // is "a Load on AN output terminal" — common-cathode (plate-load) OR
163        // cathode-follower (cathode-load) both satisfy it.
164        vec![
165            ("plate", vec![NeighborReq::required(NeighborRole::Load)]),
166            (
167                "cathode",
168                vec![
169                    NeighborReq::required(NeighborRole::Load),
170                    NeighborReq::optional(NeighborRole::Ref),
171                ],
172            ),
173            (
174                "grid",
175                vec![
176                    NeighborReq::optional(NeighborRole::Ref),
177                    NeighborReq::optional(NeighborRole::Signal),
178                ],
179            ),
180        ]
181    }
182}
183
184// ═══════════════════════════════════════════════════════════════════════════
185// Pentode
186// ═══════════════════════════════════════════════════════════════════════════
187
188#[derive(Debug, Clone, PartialEq)]
189pub struct Pentode {
190    pub model: String,
191}
192
193impl Component for Pentode {
194    impl_component_dyn!();
195
196    fn type_tag(&self) -> &'static str {
197        "pentode"
198    }
199
200    fn ports(&self) -> Vec<(&'static str, &'static str)> {
201        vec![("plate", "cathode")]
202    }
203
204    fn signal_terminals(&self) -> SignalTerminals {
205        SignalTerminals::Amplifier {
206            input: "grid",
207            output: "plate",
208            control: None,
209        }
210    }
211
212    fn is_passive(&self) -> bool {
213        false
214    }
215
216    fn is_nonlinear(&self) -> bool {
217        true
218    }
219
220    fn pin_config(&self) -> PinConfig {
221        PinConfig {
222            valid_pins: &["plate", "cathode", "g1", "g2", "grid", "screen", "vg1k"],
223            aliases: &[],
224        }
225    }
226
227    fn modulation_pins(&self) -> &'static [&'static str] {
228        &["vg1k"]
229    }
230
231    fn graph_role(&self) -> GraphRole {
232        GraphRole::CoupledEdge {
233            edge_pin_a: "plate",
234            edge_pin_b: "cathode",
235            coupled_pins: &["plate", "cathode", "grid", "screen"],
236        }
237    }
238
239    fn stamp_mna(
240        &self,
241        _comp_id: &str,
242        _n1: Option<usize>,
243        _n2: Option<usize>,
244        _mna: &mut MnaSystem,
245        _sample_rate: f64,
246    ) -> StampResult {
247        StampResult::Skip
248    }
249
250    fn edges(&self) -> Vec<ComponentEdge> {
251        vec![ComponentEdge {
252            pin_a: "plate",
253            pin_b: "cathode",
254            kind: EdgeKind::Nonlinear,
255            port_group: None,
256        }]
257    }
258
259    fn classify_nonlinear(
260        &self,
261        comp_id: &str,
262        node_a: NodeId,
263        node_b: NodeId,
264        _gnd_node: NodeId,
265        node_names: &HashMap<String, NodeId>,
266    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
267        let grid_node = node_names
268            .get(&format!("{comp_id}.grid",))
269            .or_else(|| node_names.get(&format!("{comp_id}.g1")))
270            .copied();
271        Some((
272            NonlinearKind::Pentode {
273                model_name: self.model.clone(),
274                plate_node: node_a,
275                cathode_node: node_b,
276                grid_node,
277            },
278            vec![node_a, node_b],
279        ))
280    }
281
282    fn modulation_sink(&self, pin: &str) -> Option<ModulationSink> {
283        match pin {
284            "vg1k" => Some(ModulationSink {
285                target_kind: ModulationSinkKind::PentodeVg1k,
286                bias: -2.0,
287                range: 2.0,
288            }),
289            _ => None,
290        }
291    }
292
293    fn footprint_ref(&self) -> (&'static str, &'static str) {
294        ("Valve:Triode", "V")
295    }
296
297    fn symbol_name(&self) -> &'static str {
298        "pentode"
299    }
300    fn layout_class(&self) -> &'static str {
301        "pentode"
302    }
303
304    fn pin_direction(&self, pin: &str) -> PinDirection {
305        match pin {
306            "grid" => PinDirection::Input,
307            "plate" => PinDirection::Output,
308            "screen" => PinDirection::Up,
309            "cathode" => PinDirection::Down,
310            _ => PinDirection::Bidirectional,
311        }
312    }
313
314    fn is_tube(&self) -> bool {
315        true
316    }
317    fn is_gain_device(&self) -> bool {
318        true
319    }
320    fn k_method_spec(&self) -> Option<KMethodSpec> {
321        Some(KMethodSpec {
322            axes: K_AXIS_INCIDENT_CONTROL_2D,
323            reason: "tube: 2D memoryless I-V",
324        })
325    }
326    fn model_name(&self) -> Option<&str> {
327        Some(&self.model)
328    }
329
330    fn terminal_requirements(&self) -> Vec<(&'static str, Vec<NeighborReq>)> {
331        vec![
332            ("plate", vec![NeighborReq::required(NeighborRole::Load)]),
333            (
334                "cathode",
335                vec![
336                    NeighborReq::required(NeighborRole::Load),
337                    NeighborReq::optional(NeighborRole::Ref),
338                ],
339            ),
340            (
341                "screen",
342                vec![NeighborReq::optional(NeighborRole::Ref)],
343            ),
344            (
345                "grid",
346                vec![
347                    NeighborReq::optional(NeighborRole::Ref),
348                    NeighborReq::optional(NeighborRole::Signal),
349                ],
350            ),
351        ]
352    }
353}
354
355// ═══════════════════════════════════════════════════════════════════════════
356// VariMu
357// ═══════════════════════════════════════════════════════════════════════════
358
359#[derive(Debug, Clone, PartialEq)]
360pub struct VariMu {
361    pub model: String,
362}
363
364impl Component for VariMu {
365    impl_component_dyn!();
366
367    fn type_tag(&self) -> &'static str {
368        "variable-mu triode"
369    }
370
371    fn ports(&self) -> Vec<(&'static str, &'static str)> {
372        vec![("plate", "cathode")]
373    }
374
375    fn signal_terminals(&self) -> SignalTerminals {
376        SignalTerminals::Amplifier {
377            input: "grid",
378            output: "plate",
379            control: None,
380        }
381    }
382
383    fn is_passive(&self) -> bool {
384        false
385    }
386
387    fn is_nonlinear(&self) -> bool {
388        true
389    }
390
391    fn pin_config(&self) -> PinConfig {
392        PinConfig {
393            valid_pins: &["plate", "cathode", "grid", "vgk"],
394            aliases: &[],
395        }
396    }
397
398    fn modulation_pins(&self) -> &'static [&'static str] {
399        &["vgk"]
400    }
401
402    fn graph_role(&self) -> GraphRole {
403        GraphRole::CoupledEdge {
404            edge_pin_a: "plate",
405            edge_pin_b: "cathode",
406            coupled_pins: &["plate", "cathode", "grid"],
407        }
408    }
409
410    fn stamp_mna(
411        &self,
412        _comp_id: &str,
413        _n1: Option<usize>,
414        _n2: Option<usize>,
415        _mna: &mut MnaSystem,
416        _sample_rate: f64,
417    ) -> StampResult {
418        StampResult::Skip
419    }
420
421    fn edges(&self) -> Vec<ComponentEdge> {
422        vec![ComponentEdge {
423            pin_a: "plate",
424            pin_b: "cathode",
425            kind: EdgeKind::Nonlinear,
426            port_group: None,
427        }]
428    }
429
430    fn classify_nonlinear(
431        &self,
432        comp_id: &str,
433        node_a: NodeId,
434        node_b: NodeId,
435        _gnd_node: NodeId,
436        node_names: &HashMap<String, NodeId>,
437    ) -> Option<(NonlinearKind, Vec<NodeId>)> {
438        let grid_node = node_names.get(&format!("{}.grid", comp_id)).copied();
439        Some((
440            NonlinearKind::Triode {
441                model_name: self.model.clone(),
442                plate_node: node_a,
443                cathode_node: node_b,
444                grid_node,
445                parallel_count: 1,
446                is_vari_mu: true,
447            },
448            vec![node_a, node_b],
449        ))
450    }
451
452    fn modulation_sink(&self, pin: &str) -> Option<ModulationSink> {
453        match pin {
454            "vgk" => Some(ModulationSink {
455                target_kind: ModulationSinkKind::VariMuVgk,
456                bias: -2.0,
457                range: 2.0,
458            }),
459            _ => None,
460        }
461    }
462
463    fn footprint_ref(&self) -> (&'static str, &'static str) {
464        ("Valve:Triode", "V")
465    }
466
467    fn symbol_name(&self) -> &'static str {
468        "triode"
469    }
470    fn layout_class(&self) -> &'static str {
471        "vari_mu"
472    }
473
474    fn pin_direction(&self, pin: &str) -> PinDirection {
475        match pin {
476            "grid" => PinDirection::Input,
477            "plate" => PinDirection::Output,
478            "cathode" => PinDirection::Down,
479            _ => PinDirection::Bidirectional,
480        }
481    }
482
483    fn is_tube(&self) -> bool {
484        true
485    }
486    fn is_gain_device(&self) -> bool {
487        true
488    }
489    fn k_method_spec(&self) -> Option<KMethodSpec> {
490        Some(KMethodSpec {
491            axes: K_AXIS_INCIDENT_CONTROL_CONTROL_3D,
492            reason: "pentode: 3D memoryless I-V (Vg1k, Vg2k, Vpk)",
493        })
494    }
495    fn model_name(&self) -> Option<&str> {
496        Some(&self.model)
497    }
498
499    fn terminal_requirements(&self) -> Vec<(&'static str, Vec<NeighborReq>)> {
500        // Vari-mu (e.g. 6386 / remote-cutoff) is used as a gain-reduction
501        // element; like a triode it needs a Load on an output terminal.
502        vec![
503            ("plate", vec![NeighborReq::required(NeighborRole::Load)]),
504            (
505                "cathode",
506                vec![
507                    NeighborReq::required(NeighborRole::Load),
508                    NeighborReq::optional(NeighborRole::Ref),
509                ],
510            ),
511            (
512                "grid",
513                vec![
514                    NeighborReq::optional(NeighborRole::Ref),
515                    NeighborReq::optional(NeighborRole::Signal),
516                ],
517            ),
518        ]
519    }
520}